学習用データと検証用データとを分離する module の実装

## はじめに
こんにちは。現在私はデータのカテゴライズに関する研究をやっております。
その際に、実験用データを学習用データと検証用データとに分ける処理を実装したので記載します。
実験用データはラベル (カテゴリー) 付きのものを想定しています。

## 詳細
分離の仕方は以下の二つの方法想定しました。
1, 学習用と検証用との比率をこちらで指定して分離する場合
   -> この場合は指定された比率でカテゴリーごとに分離していくことになる
2, 完全にランダムに分離する場合

1 の方をわかりやすくするために例をあげる。
例えばこんな感じの実験用データがあったとする。
c1  data1
c1  data2
c1  data3
c1  data4
c2  data5
c2  data6
c2  data7
c2  data8

このデータを比率を 0.5 に指定して分離する。
結果:
学習用データ ->
c1  data1
c1  data2
c2  data5
c2  data7

検証用データ ->
c1  data3
c1  data4
c2  data6
c2  data8

こんなかんじにカテゴリーごとに綺麗に 1:1 で分離される。

## 実装
基本的にデータは tsv を想定している。

````lang=data_separator.rb
require "set"

# 重複のない数値配列を返す
def get_uniq_numbers(min, max, noa)
  # min: 乱数の下限, max: 乱数の上限, noa: 必要な乱数の個数
  # min < max でない場合は空配列を返す
  return [] if min >= max

  numbers = Set.new
  i = 0
  while i < noa
    num = rand(min..max)
    if !numbers.include?(num)
      numbers.add(num)
      i += 1
    end
  end

  numbers.to_a
end

# 学習用データ (in_sample) と検証用データ (out_of_sample) とを分離するための module
module DataSeparator

  # in_sample の比率を決めて分離する場合はこちらを使用する
  # in_sample の比率の default は 0.5
  def separate_per_category(data_set, in_sample_ratio=0.5)
    #  とりあえずカテゴリーごとにデータをまとめる
    categories = Hash.new {|h, cat| h[cat] = []}
    data_set.each do |data|
      categories[data[:category]].push(data[:data])
    end

    # ## in_sample と out_of_sample とをビルドする
    # こちらに分類したデータを入れていく
    separated_data = {in_sample: [], out_of_sample: []}
    categories.each do |category, data_array|
      # 乱数の max
      rand_max = data_array.length - 1
      # インサンプルデータの数
      in_sample_noa = (data_array.length * in_sample_ratio).round
      # in_sample にするデータの index を取得
      in_sample_indexes = get_uniq_numbers(0, rand_max, in_sample_noa)

      # あとは振り分けていくだけ
      data_array.each_with_index do |data, i|
        obj = {category: category, data: data}
        if in_sample_indexes.include?(i)
          separated_data[:in_sample].push(obj)
        else
          separated_data[:out_of_sample].push(obj)
        end
      end

    end
    separated_data
  end
  module_function :separate_per_category

  # 特に比率を決めないで分離する場合
  def separate_randomly(data_set)
    separated_data = {in_sample: [], out_of_sample: []}
    data_set.each do |data|
      # ランダムに 0 or 1 の乱数を発生させて、1 の場合は in_sample にする
      if rand(0..1) == 1
        separated_data[:in_sample].push(data)
      else
        separated_data[:out_of_sample].push(data)
      end
    end
    separated_data
  end
  module_function :separate_randomly
end

````

実際にテストしてみる。

````lang=test.rb
require_relative "data_separator.rb"

tsv_file = "./test_data.tsv"
# データセット作成
data_set = []
File.open(tsv_file) do |file|
  file.each_line do |line|
    array = line.split(/\t/)
    data_set.push({category:array.first, data: array.last.chomp})
  end
end

# カテゴリーごとに比率 0.7 で分離する
separated_per_category = DataSeparator.separate_per_category(data_set, 0.7)
p separated_per_category

print("\n")

# ランダムに分離する
separate_randomly = DataSeparator.separate_randomly(data_set)
p separate_randomly
````

実験用データが記載された test_data.tsv の中身はこんな感じ

````lang=test_data.tsv
python	機械学習でよく使用される言語です
python	広く使用されている汎用のプログラミング言語である。
python	小規模なプログラムから大規模なプログラムまで、さまざまなプログラムをクリアに書けるように、多くのコードが提供されている
python	標準ライブラリやサードパーティ製のライブラリも充実している。
python	理工学や統計解析のためのツールとしてなど、幅広い領域で使用されている。
ruby	オープンソースの動的なプログラミング言語で、 シンプルさと高い生産性を備えています。
ruby	まつもとゆきひろ（通称 Matz）により開発されたオブジェクト指向スクリプト言語であり、スクリプト言語が用いられてきた領域でのオブジェクト指向プログラミングを実現する。
ruby	機能として、クラス定義、ガベージコレクション、強力な正規表現処理、マルチスレッド、例外処理、イテレータ、クロージャ、Mixin、演算子オーバーロードなどがある。
ruby	Perl を代替可能であることが初期の段階から重視されている。
ruby	可読性を重視した構文となっている。
````

## 終わりに
最初はカテゴリーごとに比率を指定していく仕様にしたんだけど、必要ないと思い一律にしました。機会があれば使ってみてください！