準備された単語ベースの 3 グラムのファイルが 14 個あり、txt ファイルの合計サイズは 75 GB です。ngram は「;」で区切られます。単語列に続く単語は「|」で区切られます。ここで、単語が 3 つの単語シーケンスに続く頻度を数えたいと思います。データ量が多いため、できるだけ早く処理する必要があります。
私のアプローチは次のとおりです。
- ngram ごと、セパレーターごとに行を分割する
;
- ngram をセパレーターで分割
|
- ngram を 2 つのテーブル
sequences
に格納し、単語がテーブルwords
内のそのシーケンスに出現する頻度をカウントしますwords
SQL Server 2014 Express があり、テーブルの構造は次のとおりです。
[dbo].[sequences]
:Id | Sequence
[dbo].[words]
:Id | sid | word | count
シーケンス テーブルは明確である必要があります。words テーブルでsid
は、the は関連するシーケンス ID、word は単語文字列、count は int 数であり、そのシーケンスの後に単語が出現する頻度をカウントします。
私の次のソリューションでは、最初は 1 行あたり約 1 秒必要ですが、これは非常に遅くなります。Parallel を使用しようとしましたが、別のプロセスが何かを挿入しているときにテーブルがロックされているため、SQL エラーが発生します。
私のプログラム:
static void Main(string[] args)
{
DateTime begin = DateTime.Now;
SqlConnection myConnection = new SqlConnection(@"Data Source=(localdb)\Projects;Database=ngrams;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False");
myConnection.Open();
for (int i = 0; i < 14; i++)
{
using (FileStream fs = File.Open(@"F:\Documents\ngrams\prepared_" + i + ".txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
string line;
int a = 0;
while ((line = sr.ReadLine()) != null)
{
string[] ngrams = line.Split(new char[] { ';' });
foreach (string ngram in ngrams)
{
string[] gram = ngram.Split(new Char[] { '|' });
if (gram.Length > 1)
{
string sequence = gram[0];
string word = gram[1];
storeNgrams(myConnection, sequence, word);
}
}
Console.WriteLine(DateTime.Now.Subtract(begin).TotalMinutes);
a++;
}
}
}
Console.WriteLine("Processed 75 Gigabyte in hours: " + DateTime.Now.Subtract(begin).TotalHours);
}
private static void storeNgrams(SqlConnection myConnection, string sequence, string word)
{
SqlCommand insSeq = new SqlCommand("INSERT INTO sequences (sequence) VALUES (@sequence); SELECT SCOPE_IDENTITY()", myConnection);
SqlCommand insWord = new SqlCommand("INSERT INTO words (sid, word, count) VALUES (@sid, @word, @count)", myConnection);
SqlCommand updateWordCount = new SqlCommand("UPDATE words SET count = @count WHERE sid = @sid AND word = @word", myConnection);
SqlCommand searchSeq = new SqlCommand("SELECT Id from sequences WHERE sequence = @sequence", myConnection);
SqlCommand getWordCount = new SqlCommand("Select count from words WHERE sid = @sid AND word = @word", myConnection);
searchSeq.Parameters.AddWithValue("@sequence", sequence);
object searchSeq_obj = searchSeq.ExecuteScalar();
if (searchSeq_obj != null)
{
insNgram(insWord, updateWordCount, getWordCount, searchSeq_obj, word).ExecuteNonQuery();
}
else
{
insSeq.Parameters.AddWithValue("@sequence", sequence);
object sid_obj = insSeq.ExecuteScalar();
if (sid_obj != null)
{
insNgram(insWord, updateWordCount, getWordCount, sid_obj, word).ExecuteNonQuery();
}
}
}
private static SqlCommand insNgram(SqlCommand insWord, SqlCommand updateWordCount, SqlCommand getWordCount, object sid_obj, string word)
{
int sid = Convert.ToInt32(sid_obj);
getWordCount.Parameters.AddWithValue("@sid", sid);
getWordCount.Parameters.AddWithValue("@word", word);
object wordCount_obj = getWordCount.ExecuteScalar();
if (wordCount_obj != null)
{
int wordCount = Convert.ToInt32(wordCount_obj) + 1;
return storeWord(updateWordCount, sid, word, wordCount);
}
else
{
int wordCount = 1;
return storeWord(insWord, sid, word, wordCount);
}
}
private static SqlCommand storeWord(SqlCommand updateWord, int sid, string word, int wordCount)
{
updateWord.Parameters.AddWithValue("@sid", sid);
updateWord.Parameters.AddWithValue("@word", word);
updateWord.Parameters.AddWithValue("@count", wordCount);
return updateWord;
}
法外な時間を必要としないように、ngram をより速く処理するにはどうすればよいですか?
PS: 私は C# と自然言語処理がまったく初めてです。
編集 1 :サンプル ngram の要求に応じて、各行に約 4 または 5 あります (ただし、もちろん単語の組み合わせは異なります):ほぼ同じ | お気に入り;
編集 2: コードを次のように変更すると、エラーSystem.AggregateException: At least one failure occured ---> System.InvalidOperationException: There is already an open DataReader associated with this Command that must be closed first. 、ここのように。
Parallel.For(0, 14, i => sqlaction(myConnection, i, begin));
編集 3:接続文字列にMultipleActiveResultSets=trueを 追加すると、Parallel を使用してもエラーは発生しません。関連するすべてのループを同等の Parallel に置き換え、行番号 (169521628 行) を数えるだけですべてのファイルを実行し、1 行に必要な平均時間を計算しました。これは 0.051502946 秒です。それでも101日くらいはかかります!