nekoneko_gen 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,75 +1,126 @@
1
1
  # NekonekoGen
2
2
 
3
- Easy to Use Ruby Text Classifier Generator.
3
+ ネコでもテキスト分類器のRubyライブラリが生成できる便利ツール
4
4
 
5
- ## Installation
5
+ ## インストール
6
6
 
7
- Add this line to your application's Gemfile:
7
+ % gem install nekoneko_gen
8
+
9
+ でインストールできます。
10
+
11
+ または Gemfile に次の行を追加して
8
12
 
9
13
  gem 'nekoneko_gen'
10
14
 
11
- And then execute:
15
+ 次のコマンドを実行。
12
16
 
13
17
  $ bundle
14
18
 
15
- Or install it yourself as:
19
+ ## 使い方の例 (1) 2ちゃんねるの投稿からどのスレッドの投稿か判定するライブラリを生成する
20
+
21
+ 例として、2ちゃんねるに投稿されたデータから、投稿(レス)がどのスレッドのレスか判定するライブラリを生成してみます。
22
+
23
+ まず
16
24
 
17
- $ gem install nekoneko_gen
25
+ % gem install nekoneko_gen
18
26
 
19
- ## Usage
27
+ でインストールします。
28
+ Ruby 1.8.7でも1.9.2でも動きますが1.9.2のほうが5倍くらい速いので1.9.2以降がおすすめです。
29
+ 環境は、ここではUbuntuを想定しますが、Windowsでも使えます。(WindowsXP, ruby 1.9.3p0で確認)
30
+
31
+ データは僕が用意しているので、適当にdataというディレクトリを作ってダウンロードします。
20
32
 
21
33
  % mkdir data
22
34
  % cd data
23
35
  % wget -i http://www.udp.jp/misc/2ch_data/index1.txt
24
- ...
25
36
  % cd ..
37
+
38
+ でダウンロードされます。
39
+
40
+ いろいろダウンロードされますが、とりあえず、ドラクエ質問スレとラブプラス質問スレの2択にしようと思うので、以下のファイルを使用します。
41
+ これらを使って、入力された文章がドラクエ質問スレのレスか、ラブプラス質問スレのレスか判定するライブラリを生成します。
42
+
43
+ - dragon_quest.txt: ドラゴンクエストなんでも質問スレのデータ(約3万件)
44
+ - dragon_quest_test.txt: dragon_quest.txtからテスト用に500件抜いたレス(dragon_quest.txtには含まれない)
45
+ - dragon_quest_test2.txt: dragon_quest_test.txtの2レスを1行にしたデータ
46
+ - loveplus.txt: ラブプラス質問スレのデータ(約2.5万件)
47
+ - loveplus_test.txt: loveplus.txtからテスト用に500件抜いたレス
48
+ - loveplus_test2.txt: loveplus_test.txtの2レスを1行にしたデータ
49
+
50
+ 入力データのフォーマットは、1カテゴリ1ファイル1行1データです。このデータの場合は、1レス中の改行コードを消して1行1レスにしてしています。
51
+ データの整備はアンカー(>>1のようなリンク)を消しただけなので、「サンクス」「死ぬ」「そうです」みたいなどう考えても分類無理だろみたいなデータも含まれています。また突然荒らしが登場してスレと関係ないクソレスを繰り返していたりもします。
52
+ \*_test.txtと\*_test2.txtは生成されたライブラリの確認用です。*_test.txtのうちいくつ正解できるか数えるのに使います。*_test2.txtは、*_test.txtの2レスを1データにしたものです。2ちゃんの投稿は短すぎてうまく判定できないことが多いのでは? と思うので、なら2レスあれば判定できるのか? という確認用です。
53
+
54
+ ### 生成してみる
55
+
56
+ % nekoneko_gen -n game_thread_classifier data/dragon_quest.txt data/loveplus.txt
57
+
58
+ nekoneko_genというコマンドで生成します。
59
+ -nで生成する分類器の名前を指定します。これは".rb"を付けてファイル名になるのと、キャピタライズしてモジュール名になります。生成先ディレクトリを指定したい場合は、直接ファイル名でも指定できます。
60
+ その後ろに分類(判定)したい種類ごとに学習用のファイルを指定します。最低2ファイルで、それ以上ならいくつでも指定できます。
61
+
62
+ ちょっと時間がかかるので、待ちます。2分くらい。
63
+
26
64
  % nekoneko_gen -n game_thread_classifier data/dragon_quest.txt data/loveplus.txt
27
- loading data/dragon_quest.txt... 35.5426s
28
- loading data/loveplus.txt... 36.0522s
29
- step 0... 0.879858, 3.7805s
30
- step 1... 0.919624, 2.2018s
31
- step 2... 0.932147, 2.1174s
32
- step 3... 0.940959, 2.0569s
33
- step 4... 0.946985, 1.8876s
34
- step 5... 0.950891, 1.8564s
35
- step 6... 0.953541, 1.8398s
36
- step 7... 0.955464, 1.8204s
37
- step 8... 0.957427, 1.8008s
38
- step 9... 0.959056, 1.7912s
39
- step 10... 0.961098, 1.8027s
40
- step 11... 0.961745, 1.7716s
41
- step 12... 0.962943, 1.7633s
42
- step 13... 0.963610, 1.7477s
43
- step 14... 0.964611, 1.6216s
44
- step 15... 0.965259, 1.7291s
45
- step 16... 0.965730, 1.7271s
46
- step 17... 0.966613, 1.7225s
47
- step 18... 0.967241, 1.5861s
48
- step 19... 0.967712, 1.7113s
49
- DRAGON_QUEST, LOVEPLUS : 71573 features
65
+ loading data/dragon_quest.txt... 37.0108s
66
+ loading data/loveplus.txt... 37.5334s
67
+ step 0... 0.893258, 4.2150s
68
+ step 1... 0.936877, 1.8508s
69
+ step 2... 0.948048, 1.3891s
70
+ step 3... 0.954943, 1.3921s
71
+ step 4... 0.959396, 1.1686s
72
+ step 5... 0.962824, 1.3013s
73
+ step 6... 0.964833, 1.1754s
74
+ step 7... 0.966271, 1.1562s
75
+ step 8... 0.967749, 1.2547s
76
+ step 9... 0.968537, 1.1301s
77
+ step 10... 0.969581, 1.1238s
78
+ step 11... 0.970074, 1.2611s
79
+ step 12... 0.970369, 1.1102s
80
+ step 13... 0.971197, 0.9888s
81
+ step 14... 0.972162, 1.2344s
82
+ step 15... 0.972655, 1.0946s
83
+ step 16... 0.973186, 1.0937s
84
+ step 17... 0.973482, 1.1007s
85
+ step 18... 0.973896, 1.0846s
86
+ step 19... 0.973975, 1.0803s
87
+ DRAGON_QUEST, LOVEPLUS : 86497 features
50
88
  done nyan!
51
-
89
+
90
+ 終わったら -nで指定した名前のファイルにRubyのコードが生成されています。
91
+
52
92
  % ls -la
53
93
  ...
54
- -rw-r--r-- 1 ore users 2555555 2012-05-28 08:10 game_thread_classifier.rb
94
+ -rw-r--r-- 1 ore users 5000504 2012-06-04 07:20 game_thread_classifier.rb
55
95
  ...
56
-
57
- % cat > console.rb
96
+
97
+ 5MBくらいありますね。結構デカい。
98
+ このファイルには、GameThreadClassifier(指定した名前をキャピタライズしたもの)というModuleが定義されていて、self.predict(text)というメソッドを持っています。このメソッドに文字列を渡すと、予測結果としてGameThreadClassifier::DRAGON_QUESTかGameThreadClassifier::LOVEPLUSを返します。この定数名は、コマンドに指定したデータファイル名を大文字にしたものです。
99
+
100
+ ### 試してみる
101
+
102
+ 生成されたライブラリを使ってみましょう。
103
+ 注意として、Ruby 1.8.7の場合は、$KCODEを'u'にしておかないと動きません。あと入力の文字コードもutf-8のみです。
104
+
58
105
  # coding: utf-8
59
106
  if (RUBY_VERSION < '1.9.0')
60
107
  $KCODE = 'u'
61
108
  end
62
109
  require './game_thread_classifier'
110
+ require 'kconv'
63
111
 
64
112
  $stdout.sync = true
65
113
  loop do
66
114
  print "> "
67
- line = $stdin.readline
115
+ line = $stdin.readline.toutf8
68
116
  label = GameThreadClassifier.predict(line)
69
117
  puts "#{GameThreadClassifier::LABELS[label]}の話題です!!!"
70
118
  end
71
- ^D
72
-
119
+
120
+ こんなコードを console.rb として作ります。
121
+ GameThreadClassifier.predictは予測されるクラスのラベル番号を返します。
122
+ GameThreadClassifier::LABELSには、ラベル番号に対応するラベル名が入っているので、これを表示してみます。
123
+
73
124
  % ruby console.rb
74
125
  > 彼女からメールが来た
75
126
  LOVEPLUSの話題です!!!
@@ -83,8 +134,13 @@ Or install it yourself as:
83
134
  DRAGON_QUESTの話題です!!!
84
135
  > スライムを彼女にプレゼント
85
136
  LOVEPLUSの話題です!!!
86
-
87
- %cat > test.rb
137
+
138
+ できてるっぽいですね。CTRL+DとかCTRL+Cとかで適当に終わります。
139
+
140
+ ### 正解率を調べてみる
141
+
142
+ \*_test.txt、\*_test2.txtの何%くらい正解できるか調べてみます。
143
+
88
144
  if (RUBY_VERSION < '1.9.0')
89
145
  $KCODE = 'u'
90
146
  end
@@ -103,24 +159,389 @@ Or install it yourself as:
103
159
  labels.each_with_index do |c, i|
104
160
  printf "%16s: %f\n", GameThreadClassifier::LABELS[i], c.to_f / count.to_f
105
161
  end
106
- ^D
162
+
163
+ 引数に指定したファイルを1行ずつpredictに渡して、予測されたラベル番号の数を数えて、クラスごとに全体の何割かを表示するだけのコードです。
164
+ GameThreadClassifier.kは、クラス数(この場合、DRAGON_QUESTとLOVEPLUSで2)を返します。
165
+
166
+ % ruby test.rb data/dragon_quest_test.txt
167
+ DRAGON_QUEST: 0.924000
168
+ LOVEPLUS: 0.076000
169
+
170
+ data/dragon_quest_test.txtには、ドラクエ質問スレのデータしかないので、すべて正解であれば、DRAGON_QUEST: 1.0になるはずです。
171
+ DRAGON_QUEST: 0.924000なので、92.4%は正解して、7.6%はラブプラスと間違えたことが分かります。
172
+ 同じようにすべて試してみましょう。
107
173
 
108
174
  % ruby test.rb data/dragon_quest_test.txt
109
- DRAGON_QUEST: 0.932000
110
- LOVEPLUS: 0.068000
175
+ DRAGON_QUEST: 0.924000
176
+ LOVEPLUS: 0.076000
111
177
  % ruby test.rb data/loveplus_test.txt
112
- DRAGON_QUEST: 0.124000
113
- LOVEPLUS: 0.876000
178
+ DRAGON_QUEST: 0.102000
179
+ LOVEPLUS: 0.898000
180
+
114
181
  % ruby test.rb data/dragon_quest_test2.txt
115
182
  DRAGON_QUEST: 0.988000
116
183
  LOVEPLUS: 0.012000
117
184
  % ruby test.rb data/loveplus_test2.txt
118
- DRAGON_QUEST: 0.012048
119
- LOVEPLUS: 0.987952
185
+ DRAGON_QUEST: 0.004016
186
+ LOVEPLUS: 0.995984
187
+
188
+ ラブプラスはちょっと悪くて、89.8%くらいですね。平均すると、91%くらい正解しています。
189
+ また2レスで判定すると99%以上正解することが分かりました。2レスあれば、それがドラクエスレか、ラブプラススレか、ほとんど間違えることなく判定できるっぽいですね。
190
+
191
+ #### まとめ
192
+
193
+ ここまで読んでいただければ、どういうものか分かったと思います。
194
+ 用意したデータファイルを学習して、指定した文字列がどのデータファイルのデータと似ているか判定するRubyライブラリを生成します。
195
+ 生成されたライブラリは、Rubyの標準ライブラリ以外では、 json と bimyou_segmenter に依存しています。
196
+
197
+ gem install json bimyou_segmenter
198
+
199
+ C Extensionが使えない環境だと、
200
+
201
+ gem install json_pure bimyou_segmenter
202
+
203
+ とすれば、いろんな環境で生成したライブラリが使えるようになります。
204
+
205
+ ### 他のファイルも試す
206
+
207
+ データは他に skyrim.txt (スカイリムの質問スレ)、mhf.txt (モンスターハンターフロンティアオンラインの質問スレ)を用意しているので、これらも学習できます。
208
+
209
+ % nekoneko_gen -n game_thread_classifier data/dragon_quest.txt data/loveplus.txt data/skyrim.txt data/mhf.txt
210
+
211
+ 単純に指定するファイルを増やすだけです。
212
+ 生成されるコードも判定結果が増えただけなので、上で作ったconsole.rb、test.rbがそのまま使えます。
213
+
214
+ % nekoneko_gen -n game_thread_classifier data/dragon_quest.txt data/loveplus.txt data/skyrim.txt data/mhf.txt
215
+ loading data/dragon_quest.txt... 37.1598s
216
+ loading data/loveplus.txt... 37.9838s
217
+ loading data/skyrim.txt... 134.5455s
218
+ loading data/mhf.txt... 72.3003s
219
+ step 0... 0.882245, 19.6765s
220
+ step 1... 0.922662, 14.9239s
221
+ step 2... 0.932979, 14.5471s
222
+ step 3... 0.939081, 13.0942s
223
+ step 4... 0.943442, 12.2289s
224
+ step 5... 0.947011, 12.7141s
225
+ step 6... 0.950062, 12.0611s
226
+ step 7... 0.952911, 11.9480s
227
+ step 8... 0.955120, 11.3372s
228
+ step 9... 0.956726, 11.8161s
229
+ step 10... 0.958260, 11.1741s
230
+ step 11... 0.959807, 11.1724s
231
+ step 12... 0.960831, 11.6116s
232
+ step 13... 0.961533, 11.0797s
233
+ step 14... 0.962678, 10.4930s
234
+ step 15... 0.963860, 11.4895s
235
+ step 16... 0.964193, 10.9576s
236
+ step 17... 0.965106, 11.4999s
237
+ step 18... 0.965567, 10.2368s
238
+ step 19... 0.966096, 10.8386s
239
+ DRAGON_QUEST : 245796 features
240
+ LOVEPLUS : 245796 features
241
+ SKYRIM : 245796 features
242
+ MHF : 245796 features
243
+ done nyan!
120
244
 
245
+ % ruby test.rb data/dragon_quest_test.txt
246
+ DRAGON_QUEST: 0.864000
247
+ LOVEPLUS: 0.040000
248
+ SKYRIM: 0.062000
249
+ MHF: 0.034000
250
+ % ruby test.rb data/loveplus_test.txt
251
+ DRAGON_QUEST: 0.070000
252
+ LOVEPLUS: 0.832000
253
+ SKYRIM: 0.056000
254
+ MHF: 0.042000
255
+ % ruby test.rb data/skyrim_test.txt
256
+ DRAGON_QUEST: 0.046000
257
+ LOVEPLUS: 0.038000
258
+ SKYRIM: 0.860000
259
+ MHF: 0.056000
260
+ % ruby test.rb data/mhf_test.txt
261
+ DRAGON_QUEST: 0.042000
262
+ LOVEPLUS: 0.022000
263
+ SKYRIM: 0.056000
264
+ MHF: 0.880000
121
265
 
122
- % nekoneko_gen -n game_thread_classifier data/dragon_quest.txt data/loveplus.txt data/skyrim.txt data/mhf.txt
123
- ...
124
- ...
125
- ...
266
+ % ruby test.rb data/dragon_quest_test2.txt
267
+ DRAGON_QUEST: 0.968000
268
+ LOVEPLUS: 0.012000
269
+ SKYRIM: 0.008000
270
+ MHF: 0.012000
271
+ % ruby test.rb data/loveplus_test2.txt
272
+ DRAGON_QUEST: 0.000000
273
+ LOVEPLUS: 0.991968
274
+ SKYRIM: 0.008032
275
+ MHF: 0.000000
276
+ % ruby test.rb data/skyrim_test2.txt
277
+ DRAGON_QUEST: 0.004000
278
+ LOVEPLUS: 0.008000
279
+ SKYRIM: 0.976000
280
+ MHF: 0.012000
281
+ % ruby test.rb data/mhf_test2.txt
282
+ DRAGON_QUEST: 0.008032
283
+ LOVEPLUS: 0.000000
284
+ SKYRIM: 0.012048
285
+ MHF: 0.979920
286
+
287
+ 1レスの場合は、選択肢が増えた分悪くなっています。平均すると正解は86%くらいでしょうか。2レスの場合は、まだ97%以上正解しています。
288
+
289
+
290
+ ## 使い方の例 (2) 20 newsgroupsを試してみる
291
+
292
+
293
+ 文書分類では、20newsgroupsというデータセットがよく使われるようなので、試してみました。
294
+ nekoneko_genは英語テキストにも対応しています。
295
+
296
+ http://people.csail.mit.edu/jrennie/20Newsgroups/
297
+
298
+ これは20種類のニュースグループに投稿された約2万件のドキュメントを含むデータセットです(学習用が1.1万件、確認用が7.5千件だった)。
299
+ ニュースグループというのは、メーリングリストで2ちゃんねるをやっている感じのものだと思います。
300
+ 20種類の板に投稿されたレスをどの板の投稿か判定するマシンを学習します。
301
+
302
+
303
+ (注意: ここに書かれている作業用のコードはRuby1.9系でしか動きません)
304
+
305
+ ### 最新のnekoneko_genにアップデート
306
+
307
+ まず
308
+
309
+ % gem update nekoneko_gen
310
+
311
+ とアップデートします。(古いものは英語に対応していないかもしれません)
312
+ これを書いている時点の最新は0.4.1です。
313
+ 入っていない場合は、
314
+
315
+ % gem install nekoneko_gen
316
+
317
+ でインストールされます。
318
+
319
+ ### データの準備
320
+
321
+ サイトを見ると何種類かありますけど、20news-bydate.tar.gz を使います。
322
+
323
+ % wget http://people.csail.mit.edu/jrennie/20Newsgroups/20news-bydate.tar.gz
324
+ % tar -xzvf 20news-bydate.tar.gz
325
+ % ls
326
+ 20news-bydate-test 20news-bydate-train
327
+
328
+ train用とtest用に分かれているらしいので、trainで学習して、testで確認します。
329
+ 構造を見てみましょう。
330
+
331
+
332
+ % ls 20news-bydate-train
333
+ alt.atheism comp.os.ms-windows.misc comp.sys.mac.hardware misc.forsale rec.motorcycles rec.sport.hockey sci.electronics sci.space talk.politics.guns talk.politics.misc
334
+ comp.graphics comp.sys.ibm.pc.hardware comp.windows.x rec.autos rec.sport.baseball sci.crypt sci.med soc.religion.christian talk.politics.mideast talk.religion.misc
335
+ % ls 20news-bydate-train/comp.os.ms-windows.misc
336
+ 10000 9141 9159 9450 9468 9486 9506
337
+
338
+ 20news-bydate-trainと20news-bydate-testの下に各カテゴリのディレクトリがあって、各カテゴリのディレクトリにドキュメントがファイルに分かれて入っているようです。
339
+
340
+ nekoneko_genは、1ファイル1カテゴリ1行1データの入力フォーマットなので、まずこんなスクリプトで変換します。
341
+
342
+ # coding: utf-8
343
+ # 20news-conv.rb
344
+ require 'fileutils'
345
+ require 'kconv'
346
+
347
+ src = ARGV.shift
348
+ dest = ARGV.shift
349
+ unless (src && dest)
350
+ warn "20news-conv.rb srcdir destdir\n"
351
+ exit(-1)
352
+ end
353
+
354
+ FileUtils.mkdir_p(dest)
355
+ data = Hash.new
356
+ # 元データの各ファイルについて
357
+ Dir.glob("#{src}/*/*").each do |file|
358
+ if (File.file?(file))
359
+ # root/category/nに分解
360
+ root, category, n = file.split('/')[-3 .. -1]
361
+ if (root && category && n)
362
+ data[category] ||= []
363
+ # ファイルの内容を改行をスペースに置き換えて(1行にして)カテゴリのデータに追加
364
+ data[category] << NKF::nkf("-w", File.read(file)).gsub(/[\r\n]+/, ' ')
365
+ end
366
+ end
367
+ end
368
+
369
+ # 出力側で
370
+ data.each do |k,v|
371
+ # カテゴリ名.txtのファイルにデータを行単位で吐く
372
+ path = File.join(dest, "#{k}.txt")
373
+ File.open(path, "w") do |f|
374
+ f.write v.join("\n")
375
+ end
376
+ end
377
+
378
+ train、testというディレクトリに変換。
379
+
380
+ % ruby 20news-conv.rb 20news-bydate-train train
381
+ % ruby 20news-conv.rb 20news-bydate-test test
382
+ %
383
+ % ls test
384
+ alt.atheism.txt comp.sys.ibm.pc.hardware.txt misc.forsale.txt rec.sport.baseball.txt sci.electronics.txt soc.religion.christian.txt talk.politics.misc.txt
385
+ comp.graphics.txt comp.sys.mac.hardware.txt rec.autos.txt rec.sport.hockey.txt sci.med.txt talk.politics.guns.txt talk.religion.misc.txt
386
+ comp.os.ms-windows.misc.txt comp.windows.x.txt rec.motorcycles.txt sci.crypt.txt sci.space.txt talk.politics.mideast.txt
387
+ % head alt.atheism.txt
388
+
389
+ できてます。
390
+
391
+ ### 学習
392
+
393
+ 1コマンドです。ここまでの作業のことは忘れましょう。分類器の名前はnews20にしました。
394
+ trainの下を全部指定します。
395
+
396
+ % nekoneko_gen -n news20 train/*
397
+
398
+ ちょっと時間かかります。
399
+
400
+ % nekoneko_gen -n news20 train/*
401
+ loading train/alt.atheism.txt... 11.2039s
402
+ loading train/comp.graphics.txt... 10.0659s
403
+ loading train/comp.os.ms-windows.misc.txt... 24.7611s
404
+ loading train/comp.sys.ibm.pc.hardware.txt... 9.1767s
405
+ loading train/comp.sys.mac.hardware.txt... 8.3413s
406
+ loading train/comp.windows.x.txt... 13.9806s
407
+ loading train/misc.forsale.txt... 6.8255s
408
+ loading train/rec.autos.txt... 9.9041s
409
+ loading train/rec.motorcycles.txt... 9.4798s
410
+ loading train/rec.sport.baseball.txt... 9.9481s
411
+ loading train/rec.sport.hockey.txt... 14.2056s
412
+ loading train/sci.crypt.txt... 19.5707s
413
+ loading train/sci.electronics.txt... 9.6204s
414
+ loading train/sci.med.txt... 13.6632s
415
+ loading train/sci.space.txt... 14.4867s
416
+ loading train/soc.religion.christian.txt... 16.4918s
417
+ loading train/talk.politics.guns.txt... 16.2433s
418
+ loading train/talk.politics.mideast.txt... 21.8133s
419
+ loading train/talk.politics.misc.txt... 16.2976s
420
+ loading train/talk.religion.misc.txt... 10.4111s
421
+ step 0... 0.953548, 58.1906s
422
+ step 1... 0.970537, 47.1082s
423
+ step 2... 0.980550, 41.9248s
424
+ step 3... 0.985889, 37.6781s
425
+ step 4... 0.989483, 35.0408s
426
+ step 5... 0.991824, 33.8357s
427
+ step 6... 0.993727, 30.1385s
428
+ step 7... 0.995139, 29.5926s
429
+ step 8... 0.996107, 29.3976s
430
+ step 9... 0.997182, 27.9107s
431
+ step 10... 0.997546, 27.1800s
432
+ step 11... 0.998004, 26.4783s
433
+ step 12... 0.998581, 26.7023s
434
+ step 13... 0.998985, 25.9511s
435
+ step 14... 0.999145, 24.7697s
436
+ step 15... 0.999324, 24.7991s
437
+ step 16... 0.999430, 24.8879s
438
+ step 17... 0.999569, 24.7246s
439
+ step 18... 0.999622, 25.2175s
440
+ step 19... 0.999615, 23.5225s
441
+ ALT_ATHEISM : 153334 features
442
+ COMP_GRAPHICS : 153334 features
443
+ COMP_OS_MS_WINDOWS_MISC : 153334 features
444
+ COMP_SYS_IBM_PC_HARDWARE : 153334 features
445
+ COMP_SYS_MAC_HARDWARE : 153334 features
446
+ COMP_WINDOWS_X : 153334 features
447
+ MISC_FORSALE : 153334 features
448
+ REC_AUTOS : 153334 features
449
+ REC_MOTORCYCLES : 153334 features
450
+ REC_SPORT_BASEBALL : 153334 features
451
+ REC_SPORT_HOCKEY : 153334 features
452
+ SCI_CRYPT : 153334 features
453
+ SCI_ELECTRONICS : 153334 features
454
+ SCI_MED : 153334 features
455
+ SCI_SPACE : 153334 features
456
+ SOC_RELIGION_CHRISTIAN : 153334 features
457
+ TALK_POLITICS_GUNS : 153334 features
458
+ TALK_POLITICS_MIDEAST : 153334 features
459
+ TALK_POLITICS_MISC : 153334 features
460
+ TALK_RELIGION_MISC : 153334 features
461
+ done nyan!
462
+
463
+ 終わったらnews20.rbというRubyのライブラリが生成されています。
464
+
465
+ % ls -la news20.rb
466
+ -rw-r--r-- 1 ore users 66599221 2012-06-02 17:10 news20.rb
467
+
468
+ 60MB以上あります。デカい。
469
+
470
+ ### 確認
471
+
472
+ 20カテゴリもあって前回のスクリプトで1カテゴリずつ見るのはきついので、一気に確認するスクリプトを書きました。
473
+
474
+
475
+ # coding: utf-8
476
+ # test.rb
477
+
478
+ # 分類器を読み込む
479
+ require './news20'
480
+
481
+ # ファイル名をnekoneko_genが返すラベル名に変換する関数
482
+ def label_name(file)
483
+ File.basename(file, ".txt").gsub(/[\.\-]/, "_").upcase
484
+ end
485
+
486
+ count = 0
487
+ correct = 0
488
+ # 指定された各ファイルについて
489
+ ARGV.each do |file|
490
+ # ファイル名からラベル名を得る
491
+ name = label_name(file)
492
+
493
+ # ラベル名から正解ラベル(定数)に変換
494
+ # (News20::LABELSにラベル番号順のラベル名があるので添え字位置を探す)
495
+ correct_label = News20::LABELS.each_with_index.select{|v,i| v == name}.flatten.pop
496
+
497
+ file_count = 0
498
+ file_correct = 0
499
+ # ファイルの各行データについて
500
+ File.read(file).lines do |l|
501
+ # 予測
502
+ label = News20.predict(l)
503
+ # ラベルが一致していたら
504
+ if (label == correct_label)
505
+ # 正解!!
506
+ file_correct += 1
507
+ end
508
+ # データ数
509
+ file_count += 1
510
+ end
511
+ correct += file_correct
512
+ count += file_count
513
+ # ファイルの内での正解率を表示
514
+ printf("%26s: %f\n", name, file_correct.to_f / file_count.to_f)
515
+ end
516
+
517
+ # 全体の正解率を表示
518
+ printf("\nAccuracy: %f\n", correct.to_f / count.to_f)
519
+
520
+ testの下を全部指定します。
521
+
522
+ % ruby test.rb test/*
523
+ ALT_ATHEISM: 0.789969
524
+ COMP_GRAPHICS: 0.825193
525
+ COMP_OS_MS_WINDOWS_MISC: 0.753807
526
+ COMP_SYS_IBM_PC_HARDWARE: 0.778061
527
+ COMP_SYS_MAC_HARDWARE: 0.867532
528
+ COMP_WINDOWS_X: 0.815190
529
+ MISC_FORSALE: 0.902564
530
+ REC_AUTOS: 0.916667
531
+ REC_MOTORCYCLES: 0.969849
532
+ REC_SPORT_BASEBALL: 0.957179
533
+ REC_SPORT_HOCKEY: 0.984962
534
+ SCI_CRYPT: 0.952020
535
+ SCI_ELECTRONICS: 0.778626
536
+ SCI_MED: 0.881313
537
+ SCI_SPACE: 0.936548
538
+ SOC_RELIGION_CHRISTIAN: 0.937186
539
+ TALK_POLITICS_GUNS: 0.934066
540
+ TALK_POLITICS_MIDEAST: 0.914894
541
+ TALK_POLITICS_MISC: 0.616129
542
+ TALK_RELIGION_MISC: 0.677291
543
+
544
+ Accuracy: 0.866171
545
+
546
+ 86.6%でした。
126
547
 
data/lib/nekoneko_gen.rb CHANGED
@@ -41,7 +41,7 @@ module NekonekoGen
41
41
  return -1
42
42
  end
43
43
  end
44
- o.on('-p C', "parameter (default AROW::R=6.0, PA2::C=1.0, MLP::HIDDEN_UNIT=K)") do |v|
44
+ o.on('-p C', "parameter (default AROW::R=10.0, PA2::C=1.0, MLP::HIDDEN_UNIT=K)") do |v|
45
45
  c = v.to_f
46
46
  end
47
47
  o.on('-q', "quiet") do
@@ -4,10 +4,10 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'linear_classifier'))
4
4
  module NekonekoGen
5
5
  # Adaptive Regularization of Weight Vector
6
6
  class Arow < LinearClassifier
7
- R = 6.0
7
+ R = 10.0
8
8
  DEFAULT_ITERATION = 20
9
9
 
10
- def initialize(k, options = {})
10
+ def initialize(k, n, options = {})
11
11
  @r = options[:c] || R
12
12
  @k = k
13
13
  @cov = []
@@ -15,14 +15,14 @@ module NekonekoGen
15
15
  @w = []
16
16
  @bias = []
17
17
  if (@k == 2)
18
- @cov[0] = Hash.new(1.0)
19
- @w[0] = Hash.new(0.0)
18
+ @cov[0] = Array.new(n, 1.0)
19
+ @w[0] = Array.new(n, 0.0)
20
20
  @covb[0] = 1.0
21
21
  @bias[0] = 0.0
22
22
  else
23
23
  k.times do |i|
24
- @cov[i] = Hash.new(1.0)
25
- @w[i] = Hash.new(0.0)
24
+ @cov[i] = Array.new(n, 1.0)
25
+ @w[i] = Array.new(n, 0.0)
26
26
  @covb[i] = 1.0
27
27
  @bias[i] = 0.0
28
28
  end
@@ -33,6 +33,7 @@ module NekonekoGen
33
33
  cov = @cov[i]
34
34
  covb = @covb[i]
35
35
  bias = @bias[i]
36
+
36
37
  y = label == i ? 1 : -1
37
38
  score = bias + dot(vec, w)
38
39
  alpha = 1.0 - y * score
@@ -2,7 +2,7 @@
2
2
  module NekonekoGen
3
3
  class Classifier
4
4
  attr_reader :k
5
- def parameter_code(index_converter = nil)
5
+ def parameter_code
6
6
  raise NotImplementedError
7
7
  end
8
8
  def classify_method_code
@@ -5,15 +5,15 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'mlp'))
5
5
 
6
6
  module NekonekoGen
7
7
  module ClassifierFactory
8
- def self.create(k, options)
8
+ def self.create(k, n, options)
9
9
  method = options[:method] || :arow
10
10
  case (method)
11
11
  when :arow
12
- Arow.new(k, options)
12
+ Arow.new(k, n, options)
13
13
  when :pa, :pa1, :pa2
14
- PA.new(k, options)
14
+ PA.new(k, n, options)
15
15
  when :mlp
16
- MLP.new(k, options)
16
+ MLP.new(k, n, options)
17
17
  else
18
18
  raise ArgumentError
19
19
  end
@@ -14,19 +14,6 @@ module NekonekoGen
14
14
  end
15
15
  dot
16
16
  end
17
- def strip!
18
- @w.each {|w|
19
- w.reject!{|k,v|
20
- if (v.abs < Float::EPSILON)
21
- # p v
22
- true
23
- else
24
- false
25
- end
26
- }
27
- }
28
- @w
29
- end
30
17
  def update(vec, label)
31
18
  loss = 0.0
32
19
  if (@k == 2)
@@ -46,20 +33,16 @@ module NekonekoGen
46
33
  w[i].size
47
34
  end
48
35
  end
49
- def parameter_code(lang, index_converter = lambda{|i| i})
36
+ def parameter_code(lang = :ruby)
50
37
  lang ||= :ruby
51
38
  case lang
52
39
  when :ruby
53
40
  else
54
41
  raise NotImplementedError
55
42
  end
56
-
57
- wvec = self.strip!.map {|w|
58
- w.reduce({}) {|h, kv| h[index_converter.call(kv[0])] = kv[1]; h }
59
- }
60
43
  <<CODE
61
44
  BIAS = #{self.bias.inspect}
62
- W = JSON.load(#{wvec.to_json.inspect})
45
+ W = JSON.load(#{@w.to_json.inspect})
63
46
  CODE
64
47
  end
65
48
  def classify_method_code(lang)
@@ -69,14 +52,14 @@ CODE
69
52
  else
70
53
  raise NotImplementedError
71
54
  end
72
-
73
55
  <<CODE
74
- def self.classify(vec)
56
+ def self.classify(svec)
75
57
  if (K == 2)
76
- BIAS[0] + W[0].values_at(*vec).compact.reduce(0.0, :+) > 0.0 ? 0 : 1
58
+ w0 = W[0]
59
+ (BIAS[0] + svec.map{|k, v| v * w0[k]}.reduce(0.0, :+)) > 0.0 ? 0 : 1
77
60
  else
78
61
  W.each_with_index.map {|w, i|
79
- [BIAS[i] + w.values_at(*vec).compact.reduce(0.0, :+), i]
62
+ [BIAS[i] + svec.map{|k, v| v * w[k]}.reduce(0.0, :+), i]
80
63
  }.max.pop
81
64
  end
82
65
  end
@@ -13,7 +13,7 @@ module NekonekoGen
13
13
  def default_hidden_unit
14
14
  @k
15
15
  end
16
- def initialize(k, options)
16
+ def initialize(k, n, options)
17
17
  @k = k
18
18
  @output_units = @k == 2 ? 1 : @k
19
19
  @hidden_units = (options[:c] || default_hidden_unit).to_i
@@ -22,15 +22,18 @@ module NekonekoGen
22
22
  @input_bias = []
23
23
  @hidden_bias = []
24
24
  @hidden_units.times do |i|
25
- @input[i] = Hash.new {|hash, key| hash[key] = default_value }
26
- @input_bias[i] = default_value
25
+ input = @input[i] = []
26
+ n.times do |j|
27
+ input[j] = rand_value
28
+ end
29
+ @input_bias[i] = rand_value
27
30
  end
28
31
  @output_units.times do |i|
29
- @hidden[i] = []
32
+ hidden = @hidden[i] = []
30
33
  @hidden_units.times do |j|
31
- @hidden[i][j] = default_value
34
+ hidden[j] = rand_value
32
35
  end
33
- @hidden_bias[i] = default_value
36
+ @hidden_bias[i] = rand_value
34
37
  end
35
38
  end
36
39
  def update(vec, label)
@@ -115,7 +118,7 @@ module NekonekoGen
115
118
  def sigmoid(a)
116
119
  1.0 / (1.0 + Math.exp(-a))
117
120
  end
118
- def default_value
121
+ def rand_value
119
122
  (rand - 0.5)
120
123
  end
121
124
  def noise
@@ -124,22 +127,18 @@ module NekonekoGen
124
127
  def default_iteration
125
128
  DEFAULT_ITERATION
126
129
  end
127
- def parameter_code(lang, index_converter = lambda{|i| i})
130
+ def parameter_code(lang = :ruby)
128
131
  lang ||= :ruby
129
132
  case lang
130
133
  when :ruby
131
134
  else
132
135
  raise NotImplementedError
133
136
  end
134
-
135
- wvec = @input.map {|w|
136
- w.reduce({}) {|h, kv| h[index_converter.call(kv[0])] = kv[1]; h }
137
- }
138
137
  <<CODE
139
138
  HIDDEN_UNITS = #{@hidden_units}
140
139
  INPUT_BIAS = #{@input_bias.inspect}
141
140
  HIDDEN_BIAS = #{@hidden_bias.inspect}
142
- INPUT_W = JSON.load(#{wvec.to_json.inspect})
141
+ INPUT_W = JSON.load(#{@input.to_json.inspect})
143
142
  HIDDEN_W = #{@hidden.inspect}
144
143
  CODE
145
144
  end
@@ -151,11 +150,12 @@ CODE
151
150
  raise NotImplementedError
152
151
  end
153
152
  <<CODE
154
- def self.classify(vec)
153
+ def self.classify(svec)
155
154
  input_y = []
156
155
  HIDDEN_UNITS.times do |i|
156
+ w = INPUT_W[i]
157
157
  input_y[i] = sigmoid(INPUT_BIAS[i] +
158
- INPUT_W[i].values_at(*vec).compact.reduce(0.0, :+))
158
+ svec.map{|k,v| v * w[k]}.reduce(0.0, :+))
159
159
  end
160
160
  if (K == 2)
161
161
  HIDDEN_BIAS[0] +
@@ -8,17 +8,17 @@ module NekonekoGen
8
8
  NORM = 2.0 # norm + BIAS
9
9
  DEFAULT_ITERATION = 20
10
10
 
11
- def initialize(k, options = {})
11
+ def initialize(k, n, options = {})
12
12
  @k = k
13
13
  @c = options[:c] || C
14
14
  @w = []
15
15
  @bias = []
16
16
  if (@k == 2)
17
- @w[0] = Hash.new(0.0)
17
+ @w[0] = Array.new(n, 0.0)
18
18
  @bias[0] = 0.0
19
19
  else
20
20
  k.times do |i|
21
- @w[i] = Hash.new(0.0)
21
+ @w[i] = Array.new(n, 0.0)
22
22
  @bias[i] = 0.0
23
23
  end
24
24
  end
@@ -39,7 +39,7 @@ module NekonekoGen
39
39
  end
40
40
  end
41
41
  def pa2(y, l)
42
- y * (l / NORM + 0.5 / @c)
42
+ y * (l / (NORM + 0.5 / @c))
43
43
  end
44
44
  def pa1(y, l)
45
45
  y * [@c, (l / NORM)].min
@@ -50,6 +50,7 @@ module NekonekoGen
50
50
  def update_at(i, vec, label)
51
51
  y = label == i ? 1 : -1
52
52
  w = @w[i]
53
+
53
54
  score = @bias[i] + dot(vec, w)
54
55
  l = 1.0 - score * y
55
56
  if (l > 0.0)
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ require 'json'
2
3
  require 'nkf'
3
4
  require 'bimyou_segmenter'
4
5
 
@@ -14,14 +15,16 @@ module NekonekoGen
14
15
  @files = files
15
16
  @word2id = {}
16
17
  @id2word = {}
17
- @classifier = ClassifierFactory.create(files.size, options)
18
+ @classifier = nil
19
+ @k = files.size
18
20
  @name = safe_name(@filename).split("_").map(&:capitalize).join
19
21
  @labels = files.map {|file| "#{safe_name(file).upcase}"}
22
+ @idf = {}
20
23
  end
21
24
  def train(iteration = nil)
22
- iteration ||= @classifier.default_iteration
23
25
  data = []
24
- @classifier.k.times do |i|
26
+ word_count = Hash.new(0)
27
+ @k.times do |i|
25
28
  t = Time.now
26
29
  data[i] = []
27
30
  print "loading #{@files[i]}... "
@@ -39,8 +42,36 @@ module NekonekoGen
39
42
  end
40
43
  puts sprintf("%.4fs", Time.now - t)
41
44
  end
45
+ data_min = data.map{|v| v.size}.min
46
+ data.each do |cd|
47
+ w = data_min / cd.size.to_f
48
+ cd.each do |vec|
49
+ vec.keys.each do |k|
50
+ word_count[k] += w
51
+ end
52
+ end
53
+ end
54
+ document_count = data_min * data.size
55
+ @idf = Array.new(0, 0)
56
+ word_count.each{|k, freq|
57
+ @idf[k] = Math.log(document_count / freq) * MATH_LOG2_INV + 1.0
58
+ }
59
+ data.each do |cdata|
60
+ cdata.each do |vec|
61
+ if (vec.size >= 2)
62
+ r = 1.0 / (Math.log(vec.size) * MATH_LOG2_INV)
63
+ else
64
+ r = 1.0
65
+ end
66
+ vec.each do |k, freq|
67
+ vec[k] = Math.log(freq + 1.0) * MATH_LOG2_INV * r * @idf[k]
68
+ end
69
+ normalize(vec)
70
+ end
71
+ end
42
72
 
43
- samples = data.map{|v| v.size}.min
73
+ @classifier = ClassifierFactory.create(@k, @word2id.size, @options)
74
+ iteration ||= @classifier.default_iteration
44
75
  iteration.times do |step|
45
76
  loss = 0.0
46
77
  c = 0
@@ -48,13 +79,14 @@ module NekonekoGen
48
79
  print sprintf("step %3d...", step)
49
80
 
50
81
  @classifier.k.times.map do |i|
51
- sampling(data[i], samples).map {|vec| [vec, i] }
82
+ sampling(data[i], data_min).map {|vec| [vec, i] }
52
83
  end.flatten(1).shuffle!.each do |v|
53
84
  loss += @classifier.update(v[0], v[1])
54
85
  c += 1
55
86
  end
56
87
  print sprintf(" %.6f, %.4fs\n", 1.0 - loss / c.to_f, Time.now - t)
57
88
  end
89
+
58
90
  if (@classifier.k > 2)
59
91
  @classifier.k.times do |i|
60
92
  puts "#{@labels[i]} : #{@classifier.features(i)} features"
@@ -95,11 +127,12 @@ class #{@name}
95
127
  LABELS = #{@labels.inspect}
96
128
  K = #{@classifier.k}
97
129
  private
130
+ MATH_LOG2_INV = 1.0 / Math.log(2.0)
98
131
  def self.fv(text)
99
132
  prev = nil
100
- BimyouSegmenter.segment(text,
101
- :white_space => true,
102
- :symbol => true).map do |word|
133
+ svec = BimyouSegmenter.segment(text,
134
+ :white_space => true,
135
+ :symbol => true).map do |word|
103
136
  if (prev)
104
137
  if (NGRAM_TARGET =~ word)
105
138
  nword = [prev + word, word]
@@ -115,14 +148,43 @@ class #{@name}
115
148
  end
116
149
  word
117
150
  end
118
- end.flatten
151
+ end.flatten.map{|word| WORD_INDEX[word]}.compact.reduce(Hash.new(0)) {|h,k| h[k] += 1; h }
152
+ unless (svec.empty?)
153
+ if (svec.size >= 2)
154
+ r = 1.0 / (Math.log(svec.size) * MATH_LOG2_INV)
155
+ else
156
+ r = 1.0
157
+ end
158
+ svec.each do |k, freq|
159
+ if (idf = IDF[k])
160
+ svec[k] = Math.log(freq + 1.0) * MATH_LOG2_INV * r * idf
161
+ else
162
+ svec[k] = 0.0
163
+ end
164
+ end
165
+ normalize(svec)
166
+ else
167
+ svec
168
+ end
169
+ end
170
+ def self.normalize(svec)
171
+ norm = Math.sqrt(svec.values.map{|v| v * v }.reduce(0.0, :+))
172
+ if (norm > 0.0)
173
+ s = 1.0 / norm
174
+ svec.each do |k, v|
175
+ svec[k] = v * s
176
+ end
177
+ end
178
+ svec
119
179
  end
120
180
  #{@classifier.classify_method_code(:ruby)}
121
181
  NGRAM_TARGET = Regexp.new('(^[ァ-ヾ]+$)|(^[a-zA-Z\\-_a-zA-Z‐_0-90-9]+$)|' +
122
182
  '(^[々〇ヵヶ' + [0x3400].pack('U') + '-' + [0x9FFF].pack('U') +
123
183
  [0xF900].pack('U') + '-' + [0xFAFF].pack('U') +
124
184
  [0x20000].pack('U') + '-' + [0x2FFFF].pack('U') + ']+$)')
125
- #{@classifier.parameter_code(:ruby, lambda{|id| id2word(id) })}
185
+ IDF = JSON.load(#{@idf.to_json.inspect})
186
+ WORD_INDEX = JSON.load(#{@word2id.to_json.inspect})
187
+ #{@classifier.parameter_code(:ruby)}
126
188
  end
127
189
  MODEL
128
190
  end
@@ -234,6 +296,7 @@ MODEL
234
296
  Kernel.print s
235
297
  end
236
298
  end
299
+ MATH_LOG2_INV = 1.0 / Math.log(2.0)
237
300
  SYMBOL = Regexp.new('^[^々〇' + [0x3400].pack('U') + '-' + [0x9FFF].pack('U') +
238
301
  [0xF900].pack('U') + '-' + [0xFAFF].pack('U') +
239
302
  [0x20000].pack('U') + '-' + [0x2FFFF].pack('U') +
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  module NekonekoGen
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.2"
4
4
  end
@@ -9,11 +9,13 @@ class NekonekoGenTest < Test::Unit::TestCase
9
9
  @clean_files = []
10
10
  end
11
11
  def teardown
12
+ =begin
12
13
  @clean_files.each do |file|
13
14
  if (File.exist?(file))
14
15
  File.unlink(file)
15
16
  end
16
17
  end
18
+ =end
17
19
  end
18
20
 
19
21
  def test_mlp
@@ -28,7 +30,7 @@ class NekonekoGenTest < Test::Unit::TestCase
28
30
  gen2('arow', {:method => :arow})
29
31
  gen3('arow',{:method => :arow})
30
32
  end
31
-
33
+
32
34
  def clean!(a, b)
33
35
  if (File.exist?(a))
34
36
  File.unlink(a)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nekoneko_gen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-02 00:00:00.000000000Z
12
+ date: 2012-06-04 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bimyou_segmenter
16
- requirement: &11266280 !ruby/object:Gem::Requirement
16
+ requirement: &21850440 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 1.2.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *11266280
24
+ version_requirements: *21850440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: json
27
- requirement: &11527320 !ruby/object:Gem::Requirement
27
+ requirement: &21849840 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *11527320
35
+ version_requirements: *21849840
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: test-unit
38
- requirement: &11890980 !ruby/object:Gem::Requirement
38
+ requirement: &21849200 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *11890980
46
+ version_requirements: *21849200
47
47
  description: Japanese Text Classifier Generator
48
48
  email:
49
49
  - nagadomi@nurs.or.jp