四畳半テクノポリス

コロナのストレスで気が狂い、D進した院生

PythonでVerilogもSPICEも同時に生成しLSIの設計テストを効率化した

HDL (SystemVerilog/Verilog/VHDL/Chisel/etc.) Advent Calendar 2021 12月14日

Veriloggenとは

VeriloggenPythonVerilogを組み立てる内部DSLです。スレッドの合成やステートマシンのようなリッチな機能も持っていますが、Chiselとの最大違いはVerilogとの直交性の高さだと感じています。Chiselがscalaの文法で論理回路を書くのに対して、Veriloggenでの回路設計はPythonの使って深層学習フレームワークでネットワークを組み立てる感覚に近いと感じています。 なので、Python上でコードを書いていても、常に生成されるVerilogを意識しながら書くことになり、生成されるコードはVerilogとしても読みやすいものとなります。また、過去に書いた回路をVeriloggenで書き直すことも容易です。

github.com

研究で使っている用途

Verilogは未定義ワイヤや結線ミスが発生しやすく、デバッグに長い時間を取られてしまいがちなので、私は普段のFPGAでの開発におけるほとんどのコードをVeriloggenで書いています。 本記事ではFPGAでの回路開発以外でVeriloggenが大活躍している、LSIの設計やテストでの利用について書きます。

Verilogのテストベンチ生成時のPWLの生成

当たり前ですがデジタル回路とはHightとLowに変化するという点を除けば基本的にアナログ回路と同様です。LSIの設計の教科書を見ると、AND回路やOR回路はトランジスタの組み合わせで構成されています。 さらにXOR回路などもANDやNOTやOR等の基本的なロジック回路の組み合わせではなくトランジスタで構成され、クロスラッチといったトリッキーな回路で構成されることがあります。特にSRAMなどはプレチャージなどの動作を行う必要からアナログ的な性格が強い回路です。 しかし、デジタル回路はシーケンシャルに振る舞うため、検証においてはアナログ回路とは異なるアプローチが必要となります。論理的に考えられる状態や実用時に用いるデータを元に、テストケースを生成して検証するソフトウェアテスト的な手法が必要となります。

このような検証を行うためにVeriloggenVerilogのシミュレーションを生成しつつ、SPICEのPWL(Piece-Wise Linear)区分線形のグラフのパターンも同時生成することで、Verilog上のデジタル回路のモデルとSPICE上のモデルの実行結果を比較可能になります。このテストのフローが次の図です。

f:id:toriten1024:20211214222127p:plain
検証フロー


SPICEモデルの生成

今回は例としてLTspiceのライブラリに含まれているDフリップフロップ挙動がVerilogで記述されたDフリップフロップと一致するかのテストを行います。 SPICE上のDフリップフロップは次の回路になります。Dフリップフロップのすべての入力ピンに電圧原が接続された状態です。LTspiceではシミュレーション実行時にnetファイルというシミュレーションを行うためのスクリプトが生成されます。

f:id:toriten1024:20211214150302p:plain
LTspiceの編集画面

この時のシミュレーション用のスクリプトが次のコードになります。

* C:\path\to\project\Draft1.asc
A2 N002 0 N003 N001 N004 ~Q Q 0 DFLOP
V§D N002 0 PULSE(0 1v 0 0b 0n 2n 4n)
V§CLK N003 0 PWL(0 1v 1n 1V 1.1n 0V 2n 0V 3.1n 1V 4n 1V 4.1n 0V 5n 0V 5.1n 1V 2.1 0V)
V1 N001 0 PWL(0 0 0.1n 0 0.1001n 1v 0.5n 1v 0.5001n 0)
V§CLR N004 0 PWL(0 0 10n 0V 10.001n 1V)
.tran 20n
.backanno
.end

それぞれのVから始まっている行は電源であり、それぞれの電源のある経過時間における電圧PWLは以下のフォーマットで記述可能です。

V[電源名] [線名] 0 PWL([経過時間] [電圧] [経過時間] [電圧] [経過時間] [電圧]... )

PythonコードによるPWLの生成

いよいよVeriloggenによってVerilogのテストベンチとSPICEのコードを同時に生成するプログラムを作っていきます。ちょっと長いので区切って説明しますね。

Dフリップフロップの制御クラス

    def __init__(self,name = "DFF_ctrl"):
        self.module = vg.Module(name)
        self.CLK    = self.module.Reg("CLK")        #input CLK
        self.D      = self.module.Reg("D")             #input Data
        self.CLR    = self.module.Reg("CLR")        #reset_signal
        self.PRE    = self.module.Reg("PRE")        #input Pre

        #VerilogのDフリップフロップモジュールの読み出し
        dff_mod = make_dff_module()
        dff_inst = Submodule(self.module, dff_mod, name = "DFF_inst", 
            arg_ports=(
                    ("CLK",self.CLK),
                    ("D",self.D),
                    ("CLR",self.CLR),
                )
            )
        self.step = 1
        #リセット信号のダミー
        self.DUMMY = self.module.Reg(name + "_" +"DUMMY")
        self.timming =  vg.simulation.setup_reset(self.module, reset=self.DUMMY,period = self.step,  polarity="low")
        self.timeline_counter = 0        #経過時間のカウンター
        self.timetable = {}                   #時間ごとに変化するレジスタの値を記録
        self.width_talbe = {}                #レジスタごとのビットサイズを記録

テスト対象の回路を制御するためのテストベンチを生成するプログラムをクラスとして定義しました。 このクラスはテスト対象のモジュール接続される信号をレジスタとして保持しています。 また、SPICEを生成するために、信号の変化の時系列情報を記録するリストも所持しています。

遅延と値の更新

値の更新に関する関数群です。本来であれば継承などを使ってオーバーロードなどで(pythonにはないけど)で実装するべきだと思います。

    def delay(self, delay_time):
        self.timeline_counter += delay_time
        return  Delay(delay_time)
    
    def update(self, Reg):
        #spiceファイルのためにレジスタ毎にタイミングテープるを生成する
        if str(Reg.left) not in self.timetable: #レジスタがtimetableに記録されてない場合追加
            self.timetable[str(Reg.left)] = []
            self.width_talbe[str(Reg.left)] = Reg.left.width
            self.timetable[str(Reg.left)].append((self.timeline_counter, (Reg.right)))
        else:
            #レジスタの値をアップデータする。
            self.timetable[str(Reg.left)].append((self.timeline_counter, self.timetable[str(Reg.left)][-1][1] ))
            #spiceは折れ線でデータを宣言するので0.005ずらして新しい値を追加する
            self.timetable[str(Reg.left)].append((self.timeline_counter+0.005, (Reg.right)))
        return Reg

Verilogでは#10というように表現される遅延ですが、VeriloggenではDelayメソッドで表現されます。今回はSPICE上での遅延も記録しないといけないので、DFlipFlop_moduleクラスのdelayメソッドでラップしてクラスメンバに記録を追加します。 updateメソッドも同様に、値の書き換えが行われる際に、最後のdelayで更新されたtimeline_counterが指している時刻に値の変化を記録します。 これらの関数はVeriloggenの遅延や値の更新を引数としてとり、timeline_counter等に記録したうえでそのまま戻り値として素通りさせます、よって、次の節:シーケンス操作のようにVeriloggenのsimulationに追加する際に一度関数の中をくぐらせて使います。

シーケンス操作

    #CLRにパルスを送りDFFをクリアする
    def clear_seq(self, init_value = 0):
        self.timming.add(
            self.update(self.CLR(0)),
            self.delay(self.step),                                      
            self.update(self.CLR(1)),
            self.delay(self.step),                                      
            self.update(self.CLR(0)),

        )
    #引数valueの値にDFFの出力を変化させる
    def write(self,value = 0 ):
        self.timming.add(
            #negative
            self.update(self.CLK(0)),
            self.update(self.D(value)),
            self.delay(self.step),               
            #positive
            self.update(self.CLK(1)),
            self.delay(self.step)            
        )
        pass   

デジタル回路の操作はアドレスをセットしてクロックを立ち上げたり、SPI等のビット列のシリアル化して送信など決まったパターンを取ります。こういった操作シーケンスの生成を関数化しておくことで、アドレス走査などのテストの記述が楽になります。

Dフリップフロップの操作として

の二つのシーケンスをメソッドとして定義しています。

SPICEとしての出力

    def dump_as_spice(self, vpositive="1V"):
        spice_file = ""
        for item in self.timetable: #1bitレジスタの時            
            if(self.width_talbe[str(item)] == None):
                spice_file += "V{} {} 0 PWL(".format(item, item)
                for change in self.timetable[item]:
                    change_bit = 0 if(change[1] == 0)else vpositive
                    spice_file += "{}n {} ".format(change[0], change_bit)
                spice_file += ")\n"
            else:#マルチビットなレジスタの時
                for i in range(self.width_talbe[str(item)]): 
                    spice_file += "V{}\<{}\> {}\<{}\>  0 PWL(".format(item, i, item, i)
                    for change in self.timetable[item]:
                        change_str = '{:016b}'.format(change[1])
                        change_bit = change_str[::-1][i]
                        change_bit = 0 if(change_bit == "0")else vpositive
                        spice_file += "{}n {} ".format(change[0],change_bit )
                    spice_file += ")\n"
                spice_file += "\n"
        return spice_file

    def save_as_spice(self, filename = "sim.net"):
        #回路本体が格納されているnetファイル
        path = "./template.net"
        f = open(path)
        header = f.read()
        #netファイルにPWLによる信号の変動と、シミュレーションの最大時間を追加する
        save_data = header.format(self.dump_as_spice(),self.timeline_counter+10 )
        with open(filename, mode = "w") as save_file:
            save_file.write(save_data)

Verilogのテストベンチでの値の変化はself.timeline_counterself.timetableに記録され、この変化をSPICEのPWLに変換します。Pythonで生成しているのはテストベンチだけであり、Dフリップフロップ自体はあくまでもSPICEモデルを利用するため、SPICEファイルのテンプレートが必要となります。上で示したSPICEのnetファイルをベースに作ったテンプレートは以下の通りです。

* C:\path\to\project\Draft1.asc
A2 D 0 CLK PRE CLR ~Q Q 0 DFLOP
{}
.tran {}n
.backanno
.end

操作

#main sequence
dff_control = dff_control_module()
dff_control.reset()
dff_control.write(1)
dff_control.write(0)
dff_control.write(1)
dff_control.write(0)
dff_control.write(1)
dff_control.clear_seq()
dff_control.close()

Dフリップフロップの読み書きの操作です。1と0を交互に書き込んだ後CLRを1にしてリセットします。

SPICEとVerilogのシミュレーション

生成されたnetファイルに対して以下のコマンドを実行

&'..\path\to\LTspice\XVIIx64.exe' -b .\DFF.net

生成されたSPICEのコードをLTspiceで実行した結果

f:id:toriten1024:20211214145242p:plain
生成SPICEモデルの実行結果

同様にVerilogのシミュレーションも実行

iverilog -s DFF_ctrl DFF.v -o DFF.o
vvp DFF.o

実行結果をGtkWaveで表示したもの。

f:id:toriten1024:20211214230655p:plain
VerilogによるDフリップフロップのシミュレーション結果
おおよそ同じ結果になってますね。実際のLSIの設計で使用するモデルでは、FETや配線の寄生コイルや寄生容量を製造プロセスに合わせてより詳細に計算するので、信号の立ち上がりや立下りが滑めるなどもう少し調整が必要になる筈です。

感想

Veriloggenを扱った記事としてはかなり邪道な内容となってしまいました。そのことはここで謝っておきます。 この記事が原因でVeriloggenに関して誤解を招くと申し訳ないのですが、基本的にこの記事は私の個人的なVeriloggenの利用について述べた記事なので、正しい情報、詳細な情報は公式のGithubなどをご覧ください。 また、お金が潤沢にあるのであればVerilogA等のミックスドシグナルなシミュレータを使うべきだと思います(近年はオープンソースVerilog-Aのシミュレータなども登場してきているようですね)。

このプログラムは以下の二つの理由から開発されました。

①SPICEでデジタル回路のシミュレーションを行う上で、手作業でPWLを書くことに限界を感じた。

半導体のテスタであるCloudTestingLabがVerilogのシミュレーション結果のファイルであるvcdファイルを使ってテストパターンを生成するため、入 力・出力信号共に正しい挙動をするvcdファイルが欲しかった。

これらの目標はおおよそ達成することができたと思います。少なくとも私の開発はとても楽になりました。Veriloggenを開発してくださった高前田 伸也先生に感謝を申し上げます。

また、近年MakeLSIなどホビーで半導体を製造することが流行っていますが、トランジスタを並べてデジタル回路を開発する場面があれば、お役に立つかもしれません。 長々とありがとうございました。

付録

全体のコード

import veriloggen as vg
from veriloggen.core.module import Module
from veriloggen.core.submodule import Submodule
from veriloggen.core.vtypes import Delay, If, Int, Negedge, Posedge, Initial, Integer, Reg, Sensitive , Subst 
import sys


def addtimescale(fname):
    with open(fname) as f:
        l = f.readlines()
    l.insert(0,"`timescale 1ns/1ps \n")
    with open(fname, mode='w') as f:
        f.writelines(l)

#制御対象の回路のコントローラをモジュールとして宣言
def make_dff_module():
    m = vg.Module("dff")
    CLK = m.Input("CLK")
    CLR = m.Input("CLR")
    D = m.Input("D")
    Q   = m.OutputReg("Q")
    _Q  = m.Output("_Q")
    _Q.assign(~Q)

    m.Always( Posedge(CLK), Posedge(CLR) )(
        If(CLR == 1)(
            Q(0)
        ).Else(
            Q(D)    
        )
    )
    return m


class dff_control_module():

    def __init__(self,name = "DFF_ctrl"):
        self.module = vg.Module(name)


 
        #input CLK
        self.CLK    = self.module.Reg("CLK")
        #input Data
        self.D      = self.module.Reg("D")
        #reset_signal
        self.CLR    = self.module.Reg("CLR")
        #input Pre
        self.PRE    = self.module.Reg("PRE")

        #VerilogのDフリップフロップモジュールの読み出し
        dff_mod = make_dff_module()
        dff_inst = Submodule(self.module, dff_mod, name = "DFF_inst", 
            arg_ports=(
                    ("CLK",self.CLK),
                    ("D",self.D),
                    ("CLR",self.CLR),
                )
            )
    

        self.step = 1
        #リセット信号のダミー
        self.DUMMY = self.module.Reg(name + "_" +"DUMMY")
        self.timming =  vg.simulation.setup_reset(self.module, reset=self.DUMMY,period = self.step,  polarity="low")

        #時系列のカウンター
        self.timeline_counter = 0
        #時間ごとに変化するレジスタの値を記録
        self.timetable = {}
        #レジスタごとのビットサイズを記録
        self.width_talbe = {}

    def delay(self, delay_time):
        self.timeline_counter += delay_time
        return  Delay(delay_time)
    

    def update(self, Reg):
        #spiceファイルのためにレジスタ毎にタイミングテープるを生成する
        if str(Reg.left) not in self.timetable: #レジスタがtimetableに記録されてない場合追加
            self.timetable[str(Reg.left)] = []
            self.width_talbe[str(Reg.left)] = Reg.left.width
            self.timetable[str(Reg.left)].append((self.timeline_counter, (Reg.right)))
        else:
            #レジスタの値をアップデータする。
            self.timetable[str(Reg.left)].append((self.timeline_counter, self.timetable[str(Reg.left)][-1][1] ))
            #spiceは折れ線でデータを宣言するので0.005ずらして新しい値を追加する
            self.timetable[str(Reg.left)].append((self.timeline_counter+0.005, (Reg.right)))
        return Reg

    def reset(self, init_value = 0):
        self.timming.add(
            self.update(self.D(0)),
            self.update(self.CLK(0)),
            self.update(self.CLR(0)),
            self.update(self.PRE(0)),
            self.delay(self.step),                       
        )


    def clear_seq(self, init_value = 0):
        self.timming.add(
            self.update(self.CLR(0)),
            self.delay(self.step),                                      
            self.update(self.CLR(1)),
            self.delay(self.step),                                      
            self.update(self.CLR(0)),

        )


    def write(self,value = 0 ):
        self.timming.add(
            #negative
            self.update(self.CLK(0)),
            self.update(self.D(value)),
            self.delay(self.step),               
            #positive
            self.update(self.CLK(1)),
            self.delay(self.step)            
        )
        pass        



    def close(self):
        self.timming.add(
            vg.Systask('finish'),
        )
        vg.simulation.setup_waveform(self.module,  self.module.get_vars())


    def dump_as_spice(self, vpositive="1V"):
        spice_file = ""
        for item in self.timetable: #1bitレジスタの時            
            if(self.width_talbe[str(item)] == None):
                spice_file += "V{} {} 0 PWL(".format(item, item)
                for change in self.timetable[item]:
                    change_bit = 0 if(change[1] == 0)else vpositive
                    spice_file += "{}n {} ".format(change[0], change_bit)
                spice_file += ")\n"
            else:#マルチビットなレジスタの時
                for i in range(self.width_talbe[str(item)]): 
                    spice_file += "V{}\<{}\> {}\<{}\>  0 PWL(".format(item, i, item, i)
                    for change in self.timetable[item]:
                        change_str = '{:016b}'.format(change[1])
                        change_bit = change_str[::-1][i]
                        change_bit = 0 if(change_bit == "0")else vpositive
                        spice_file += "{}n {} ".format(change[0],change_bit )
                    spice_file += ")\n"
                spice_file += "\n"
        return spice_file

    def save_as_spice(self, filename = "sim.net"):
        #回路本体が格納されているnetファイル
        path = "./template.net"
        f = open(path)
        header = f.read()
        #netファイルにPWLによる信号の変動と、シミュレーションの最大時間を追加する
        save_data = header.format(self.dump_as_spice(),self.timeline_counter+10 )
        with open(filename, mode = "w") as save_file:
            save_file.write(save_data)

def addtimescale(fname):
    with open(fname) as f:
        l = f.read()
    l = "`timescale 1ns/1ps \n"+l
    l = l.replace("uut.vcd",sys.argv[0].split(".")[0]+".vcd")
    with open(fname, mode='w') as f:
        f.writelines(l)

#main sequence
dff_control = dff_control_module()
dff_control.reset()
dff_control.write(1)
dff_control.write(0)
dff_control.write(1)
dff_control.write(0)
dff_control.write(1)
dff_control.clear_seq()
dff_control.close()

#save as spice
print(sys.argv[0].split(".")[0]+".net")
dff_control.save_as_spice(sys.argv[0].split(".")[0]+".net")

#save as verilog
verilog =  dff_control.module.to_verilog(sys.argv[0].split(".")[0]+".v")
addtimescale(sys.argv[0].split(".")[0]+".v")

博士に進学してもうすぐ一ヶ月なので感想を書く

もうすぐ博士課程に入学して1か月になります。この1か月有意義に過ごせたかというと二週目までは割と頑張ってました。毎日12時間以上は研究室にいたし、すでに基板を1枚設計したり、先生が用意した機材じゃ解決できなかった問題を自前の機材で解決しちゃったり、2週間毎日研究室に通ってたりと7割くらいの本気度で頑張ってた気がします(僕は8割5分くらいの本気度で頑張るとサーマルスロットリングを起こしだすのでまずまずのペースだと思います)。

しかし、先週からの1週間はゴミに様でした。主に原因は学振です。学振書くのつらい。博士の入試に使った研究計画書を何とか膨らませようとしてるのですが、まあまあ大変です。何を書いたらいいかわからないし、文章もめちゃくちゃな状態です。そもそも書き始めたのが遅すぎたんです。

こんなクソみたいな申請書で先生に評価書を書いてもらうのが申し訳なさすぎるので、今日は評価書を自分で書いていました。あのくらいまとめたら先生も1時間くらいで評価書が書けるかなと思います。努力を放棄して、努力しなかったことの贖罪です。完全に逃げというか言い訳に走った行為ですね。もうだめだ。

ここ1週間何をしてたかというと、数百字書いてはネットで学振の記事を読んでました。学振について調べてみたんですが東大でも合格率25%くらいだったり、スター研究者でも三振してることがわかりました。そして、「僕なんかが通るわけないじゃないか」と泣き言を言ってたら一週間過ぎていました。一週間のうちにやった仕事がほとんどないです。

ただ、学振の申請書を書き始めてよかったなと思うことも二つありました

一つは過去の申請書フォルダをあさっていたら、優秀な若い先生の不採用なった科研費の申請書がたくさん出てきました。あんなに優秀な人でもこんなに苦労して戦ってきたんだなと反省しました。アカデミアは厳しいところです。

もう一つは学振に出すのが博士課程進学者全体の1/3ということが分かったことです。もちろん社Dなどで出せない人もいるんでしょうが、大学でPIになれば科研費の申請書を出す必要があり、たとえ企業研究者になったとしても、場合によってはNEDOの申請書を出す必要があります。だから出した人のほうが出さない人より研究者として経験を積もうとしていて偉いんです。僕もそっち側に慣れるように頑張ります。

学振つらい

とりあえず僕みたいに何をしたらいいかわからない人は以下の本を買うといいと思います。この本はおそらく現在唯一の新しい形式に対応した学振の本で、著者の学振の申請書を新しい形式に書き直したものが載っています。学振の申請書の各項目についてどのようなことを書けばいいのかや、手続きのやり方について懇切丁寧に書いてあり、僕のように右も左も分からない人間にはとても助けになると思います。

www.amazon.co.jp

 最後にもう一つ面白いことがありました。上の本を書いた大上先生のスライドを見ると「こっちがDC1/DC2/PD等 こっちは大人の科研費種目」って表現が出てくるんですね。これを見て僕はまだ研究者として大人じゃないんだなって思いました。というかポスドクって科研費出せないんですね39歳(研究者として子供)とかなんかやだな。

本日博士課程の学生になりました

 本日博士課程の学生になりました。初日ですが前日と同様に昼から通学して、一日3Dプリンタをいじって終わるという生産性のない一日を過ごしておりました。

 最近は心配事が多くあまり眠れません、最近は朝食堂にこないので寮母さんに心配されがちです。今日の寝坊の理由はというと昨日うっかりこの記事を読んでしまったので寝れなくなりました。 

寝れなくなった理由

nuc.hatenadiary.org

面白いですが、あんまり後味の良い記事では無かったです。単純に筆者と性格が合わないんだと思います。Lilian女史みたいなポジティブな文章を期待して読むと脳が破壊されると思います。

この文章を書いた筆者は、筑駒出身で、未踏ジュニアのクリエータで、東大で物理を学んだ後、ロースクールに入り、そこからGoogleに就職しており。その後医学部修士課程で学んだり、医学部で特任准教授をしたり、UT-Heartのプロジェクトに携わってSIGGRAPHで入賞したりと、化物じみた経歴です。(なんでwikipediaの記事が無いんだろう?)

Twitterでは性格が悪そうとか言われてますが、コロナの時にはマスクの輸入に走り回ったりと善人だと思います。

ツイート内容にかなり批判的な部分が多いので最初は傲慢な人だなと思ったんですが、経歴を見て、僕には見えないものが見えてるんだろうなと認識を改めさせられました(権威主義は僕の悪い癖です)。

だた、chokudai氏との中国の競プロ塾に関する議論ではちょっと先入観が強い性格が見えたので、この人の批判をすべて鵜呑みにしないほうがいいなと思いました。本人もかなり気にしているらしく、専門外の事について間違った事を言わないように、他分野の専門家の友達を沢山作ってキャリブレーションしてるようです。

精神安定剤としてchokudai氏の反論記事も貼っておきます。

chokudai.hatenablog.com

 

どうでも良いんだよそんな事は、俺は俺の人生を生きなければならない。

今日は生産性の無い一日だと言いましたが、ひとつだけ良いことがありました。あまり詳しいことは言えませんが、バイトで関わっているプロジェクトの成果を製品化あとで僕がファーストオーサーで学会に出ても良いというお許しを上司からいただきました(上司大好き愛してる)。これが上手く行けば高専助教TSMCやARMで働くといった夢に一歩近づきます。頑張るぞ

明日はちゃんと9時に通学して勉強します。おやすみなさい

オーバーワーク出来ることと出来ないこと

修論の2週間前に1週間ほど連続で徹夜しました。疲労感が凄いし、座ってるだけで値落ちするので、論文読みながらカフェで寝落ちちすると言っていた落合陽一ってこんな感じなのかなって思いました。

徹夜をした理由は報告書に必要な追試を行うことになったからです。シミュレーション値で非現実的な値になったので、別に追試なんかやらなくても良いと思っていたのですが、先生から追試を行うようお達しがあり、別に遅れても良いと言われたのに期限までに終わらせました。

 

この週は報告書の作成と追試、あと修論の作成があり1週間を俯瞰してみると

  • 月:修論作成
  • 火:ネットワークAでの追試
  • 水:ネットワークBでの追試
  • 木:報告書作成
  • 金:19時に帰って18時間寝た

こんな感じです。

  • 徹夜で作った文章なんてやはり碌なものではなく、この時書いた報告書は誤字脱字が酷く、有効数字が間違っていて、データの取扱がむちゃくちゃで凄く凹みました。
  • 火と水に書いたプログラムはどれもマトモに動作し、検算しても正しい結果を出力しました。

というわけで、僕には徹夜で出来る作業徹夜で出来ない作業があることが分かりました。

 文章作成は絶対徹夜ではやらないことにします。徹夜で文章を書くと当たり前ですが、酷い怪文章が出来上がり、てにおはは無茶苦茶だし、句読点の位置もおかしいし、文章がつながってなかったり、存在理由が分からない段落が入っていたり、まあこの文章みたいな感じになってしまいます。

 プログラミングは逆に調子が良ければ好きなだけやります。自分でもあまりちゃんと動くのでびっくりしました。というか深夜3時くらいに「このバグ明日までかかるな」と思ったバグが20分くらいで解決出来てしまったりなんだか神がかってました。自分でもなんでこんなに書けるのか不思議でした。

オンラインで同窓会をしたらすごく楽しかった

最近気分が沈みがちですが、昨日は久々に元気になれました。

東京オリンピックの川淵が森の後任になるのならないので世間はもめてましたが、この川淵って僕の高専の元理事だったりします。高専の校庭を芝生化しようとしたのが印象深かったですね。うちの高専は珍しく?体育会系の部活が弱くて、文化部がとても強い環境だったんで友人が「もっと研究費や文化部の部費にまわせ」って芝生化反対運動なんかをやってました。

 そんな思い出話をTwitterでしていたところわらわら人が集まってきて高専老害ってイベントをzoomでやることになりました。参加してみると、高専卒業以来会ってない面子が結構いたり、ずっと元気か気になってた友人とも話すことが出来ました。みんなちゃんと思い通りの進路を進んでそうでよかったです。

 高専生と話して思うのが、やっぱ高専生同士の会話って楽だなってことです。現在の大学は高校偏差値が70前後の人が多く、結構みんなエリートの集まりなので、僕も節度をもって振る舞ってるつもりなのですが、高専生同士は独特の雰囲気で、適当にみんな好き勝手言ってても話が進行するので楽です。要は社会性フィルタがいらないわけです。これは恐ろしく居心地がいいです。

 まあそんなこんなでリフレッシュすることが出来ました。正直今日もあんまり生産性は高くなかったんですが、みんなと話せたのでだいぶんマシな気がします。これからも高専同期の仲間たちは大切にしていきたいですね。

Excelでもセルをprintfみたいにフォーマットされた文章にしたい

研究においてデータって大切ですよね。データが正しく管理できていないとその実験自体が無効になってしまいます。私は最近雑に管理しすぎてちょっとやらかしてしまったんですが、今回は個人的なメモとしてExcel上でのデータの取り扱いについて書きます。

 

研究で採取したデータが論文の表になるまで大体三つくらいのフェーズを経ると思います

  1. 生データ
  2. 計算途中のデータ
  3. ドキュメントに載せる表

正しい表を作るためには三つの表を正しく管理しなければなりません。特に1から2への計算時には有効数字の問題も発生するので注意です。

 データの管理例

とりあえず架空の実験データを使ってデータ管理のやり方について説明します。表の全体はこんな感じです。

f:id:toriten1024:20210204063210p:plain

 

生データ
  生データ
 データ数 10個 20個 30個 40個 50個 60個 70個 80個 90個 100個
従来手法 10.01 12.75 29.32 32.81 65.98 70.93 75.06 77.28 80.92 85.95
提案手法 25.19 32.34 45.56 65.62 70.02 75.05 80.01 82.88 83.05 85.92

 

例えばある提案手法を使って少ないデータで従来手法より少ないデータ数でも、高い精度の計算結果が得られるようになったとします。私はこの手法がどの程度優れているか知りたかったので、とりあえず従来手法と提案手法の両方に10個から100個のデータでテストを行いその結果を記録しました。その結果が上に示した表になります。

生データなので有効数字はすべて小数点以下で共通になりますが、ここでは小数点以下2桁までを有効数字とします。


途中計算(パーセンテージの計算)
  途中計算
 データ数 10個 20個 30個 40個 50個 60個 70個 80個 90個 100個
提案手法による改善率 151.6 153.6 55.39 100 6.123 5.809 6.595 7.246 2.632 -0.035

次に従来手法に対して提案手法何パーセント改善したかを計算します。ここの計算ではROUND関数を使って有効数字の桁を丸めます。生データでは数値は有効数4桁であったため、計算結果も4桁となります。

例えばデータ数10個の時の計算は有効数字4桁だと小数点以下1桁までとなるので

=ROUND((B5/B4-1)*100,1)

と書くことが出来ます。

ドキュメントに載せる表

f:id:toriten1024:20210204061152p:plain

最後にドキュメントに載せる表です。この表の下段に注目してください。提案手法の精度の後の括弧内に従来手法から何パーセント精度が向上したかが書いてあると思います。このようにExcelにおいてもC言語のprintf文のようにフォーマットを持った文字列を生成して表示することが可能です。

 このようなフォーマットを持った文章は、ExcelにおいてはVBAの文字列の連結を使って表現することが可能です。提案手法で10個の時のセルは以下のような感じで計算しています。

=B5&"("&TEXT(B9,"0.0")&")"

使っているテクニックは以下のような感じです

  • 文字列や変数同氏は&記号で連結することが可能
  • テキストはダブルクオーテーションで囲うことによって文字列とみなされる
  • TEXT関数によって有効数字分0埋めができる

三つ目のTEXT関数はデータ数が40個の時の改善率をみてわかるように小数点以下がすべて0であっても、0で埋めることが出来ます。

まとめ・わからない事

以上が痛い目にあいながら学んだ計算のやり方です。とりあえず当分はこのやり方でデータを管理してゆこうと思います。Excelは良くも悪くもExcelの域を出ないので、可能であればRかJupyterに移行したいです。あと、SpreadsheetはJSを使えるのでjs系のデータ可視化ライブラリでデータ分析ができるいいかなとか思ってます。

現状有効数字を自分で計算しているので、だれか有効数字を自動で計算してくれる機能があれば教えてほしいです。

DarknetのYOLOをOpenCVから使う

DarkNetのYOLO

 水中ロボコンQRコードの検出のためにYOLOを使うことになったのですが、最初はnnablaというライブラリで学習を行おうとしたもののうまくいかず、本家DarkNetのYOLOを使うことのになりました。YOLOで自作のデータセットを学習させる方法はいくらでも見つかると思うのでココでは割愛します。

 YOLOはCで書かれていますが、私が去年開発した、水中ロボコン用の遠隔通信ソフトは、C++OpenCVで書かれていたため、DarkNetがC++で書かれていることは、むしろ好都合でした。

本記事はで私のようにOpenCVで書かれたプログラムにDarkNetのYOLOを組み込みたい人のために、OpenCVwebカメラから取得した画像をDarkNetで推論する例を示したいと思います。

YOLOの起動に必要なもの

 YOLOを起動するためには以下のようなファイルが必要です。

  • データ設定ファイル(*.data)  
  • 設定ファイル(*.cfg) 
  • 学習済みパラメータ(.weights )  

まず学習済みのパラメータが必要です。これは自ら学習したものを利用しても良いですし、デフォルトとのものを使っても良いです。もう一つ重要なのが、YOLOに入力する画像のサイズです。コレも学習時のパラメータと一致させて下さい。 ファイルの読み込みは次のような関数をプログラムの起動時に呼び出しました。

    list *options = read_data_cfg("自分のデータ設定ファル.data");
    char *name_list = option_find_str(options, "names", "data/names.list");
    char **names = get_labels(name_list);

    image **alphabet = load_alphabet();
    network *net = load_network("自分の設定ファイル.cfg", "自分の学習済みでーた.weights", 0);

学習済のパラメータに関しては公式のドキュメントを見てもらうのが一番良いですが、僕は以下の記事を参照しました。

qiita.com

OpenCVからDarkNetのimgへの変換

OpenCVとDarkNetの画像形式は異なるため、cv::Matからdarknetまで変換する必要があります。DarkNetはImageという独自の構造体を持っているため、OpenCVをimage型に変換してあげる必要があります。

この変換は先人がやってくださっているので以下の記事を参照して下さい

qiita.com

僕はこの記事に出てきた変換を次のような関数にまとめました。

image Mat2Image(cv::Mat src)
{
    cv::Mat FMat;

    src.convertTo(FMat, CV_32FC3, 1.0 / 255);
    std::vector<cv::Mat> tmp;
    cv::split(FMat, tmp);

    int size = FMat.size().width * FMat.size().height;
    int fsize = size * sizeof(float);

    image retval = make_image(FMat.size().width, FMat.size().height, 3);

    float *p = retval.data;

    memcpy((unsigned char *)p, tmp[2].data, fsize);
    p += size;
    memcpy((unsigned char *)p, tmp[1].data, fsize);
    p += size;
    memcpy((unsigned char *)p, tmp[0].data, fsize);

    return retval;
}

推論

darknetの推論はnetwork_predict(net, X);という関数で行われます。netの方にニューラルネットワーク、Xの方に推論対象であるimageを入力します。画像サイズは自ら学習した画像のサイズに適宜変えて下さい、それと僕の学習したネットワークがモノクロを推論対象としていたため、画像をモノクロに変換する処理が入ってしまっていますがコレも必要に応じて変更して下さい。

        cv::Mat feed(frame, rect);
        cv::resize(feed, feed, cv::Size(), 506.0 / feed.rows, 506.0 / feed.rows);

        image feed_img = Mat2Image(feed);
        float *X = feed_img.data;

        layer l = net->layers[net->n - 1];
        network_predict(net, X);

        int nboxes = 0;
        detection *dets = get_network_boxes(net, feed_img.w, feed_img.h, thresh, hier_thresh, 0, 1, &nboxes);

        printf("w is %d,h is%d, th is %f , hth is %f,box is%d\n", feed_img.w, feed_img.h, thresh, hier_thresh, nboxes);

実行

//gcc -Iinclude/ -Isrc/ -DOPENCV `pkg-config --cflags opencv` main.cpp  -DGPU -I/usr/local/cuda/include/ -DCUDNN  -Wall -Wno-unused-result -Wno-unknown-pragmas -Wfatal-errors -fPIC -Ofast -DOPENCV -DGPU -DCUDNN  libdarknet.a -o darknet -lm -pthread  `pkg-config --libs opencv` -lstdc++ -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand -lcudnn -lstdc++  libdarknet.a

#include <iostream>
#include <string>
#include <sstream>
#include <iomanip>
#include <stdexcept>
#include <vector>
#include <cmath>
#include <thread>
#include <boost/lockfree/queue.hpp>
#include <opencv2/core/utility.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/stitching.hpp>
#include <omp.h>
#include <time.h>
#include "include/darknet.h"
image Mat2Image(cv::Mat src)
{
    cv::Mat FMat;

    src.convertTo(FMat, CV_32FC3, 1.0 / 255);
    std::vector<cv::Mat> tmp;
    cv::split(FMat, tmp);

    int size = FMat.size().width * FMat.size().height;
    int fsize = size * sizeof(float);

    image retval = make_image(FMat.size().width, FMat.size().height, 3);

    float *p = retval.data;

    memcpy((unsigned char *)p, tmp[2].data, fsize);
    p += size;
    memcpy((unsigned char *)p, tmp[1].data, fsize);
    p += size;
    memcpy((unsigned char *)p, tmp[0].data, fsize);

    return retval;
}

cv::Mat draw_detections_mat(image im, cv::Mat src, detection *dets, int num, float thresh, char **names, image **alphabet, int classes)
{
    int i, j;
    cv::Mat dst;
    for (i = 0; i < num; ++i)
    {
        char labelstr[4096] = {0};
        int cls = -1;
        for (j = 0; j < classes; ++j)
        {
            if (dets[i].prob[j] > thresh)
            {
                if (cls < 0)
                {
                    strcat(labelstr, names[j]);
                    cls = j;
                }
                else
                {
                    strcat(labelstr, ", ");
                    strcat(labelstr, names[j]);
                }
                printf("%s: %.0f%%\n", names[j], dets[i].prob[j] * 100);
            }
        }
        if (cls >= 0)
        {
            int width = im.h * .006;

            /*
               if(0){
               width = pow(prob, 1./2.)*10+1;
               alphabet = 0;
               }
             */

            //printf("%d %s: %.0f%%\n", i, names[class], prob*100);
            int offset = cls * 123457 % classes;
            /*
            float red = get_color(2, offset, classes);
            float green = get_color(1, offset, classes);
            float blue = get_color(0, offset, classes);
            */
            float rgb[3];

            //width = prob*20+2;

            rgb[0] = 0;
            rgb[1] = 0;
            rgb[2] = 0;
            box b = dets[i].bbox;
            //printf("%f %f %f %f\n", b.x, b.y, b.w, b.h);

            int left = (b.x - b.w / 2.) * im.w;
            int right = (b.x + b.w / 2.) * im.w;
            int top = (b.y - b.h / 2.) * im.h;
            int bot = (b.y + b.h / 2.) * im.h;

            if (left < 0)
                left = 0;
            if (right > im.w - 1)
                right = im.w - 1;
            if (top < 0)
                top = 0;
            if (bot > im.h - 1)
                bot = im.h - 1;
            cv::rectangle(src, cv::Point(left, top), cv::Point(right, bot), cv::Scalar(255, 255, 0), 5, 8);
        }
    }
    return src;
}

int main(void)
{

    std::cout << "start" << std::endl;
    float thresh = 0.9;
    float hier_thresh = 0.5;

    cv::VideoCapture streaming(0);
    cv::Mat frame;

    list *options = read_data_cfg("cfg/task/datasets.data");
    char *name_list = option_find_str(options, "names", "data/names.list");
    char **names = get_labels(name_list);

    image **alphabet = load_alphabet();
    network *net = load_network("cfg/task/yolov3-voc.cfg", "cfg/task/backup/yolov3-voc.backup", 0);

    set_batch_network(net, 1);
    srand(2222222);
    double time;
    char buff[256];
    char *input = buff;
    float nms = .45;

    streaming >> frame;
    std::cout << frame.cols << ":" << frame.rows << std::endl;

    cv::Rect rect = cv::Rect((frame.cols / 2) - frame.rows / 2, 0, frame.rows, frame.rows);

    while (1)
    {
        std::cout << "running" << std::endl;
        streaming >> frame;
        cv::cvtColor(frame, frame, cv::COLOR_RGB2GRAY);
        cv::cvtColor(frame, frame, cv::COLOR_GRAY2RGB);

        cv::Mat feed(frame, rect);
        cv::resize(feed, feed, cv::Size(), 506.0 / feed.rows, 506.0 / feed.rows);

        image feed_img = Mat2Image(feed);
        float *X = feed_img.data;

        layer l = net->layers[net->n - 1];
        network_predict(net, X);

        int nboxes = 0;
        detection *dets = get_network_boxes(net, feed_img.w, feed_img.h, thresh, hier_thresh, 0, 1, &nboxes);

        printf("w is %d,h is%d, th is %f , hth is %f,box is%d\n", feed_img.w, feed_img.h, thresh, hier_thresh, nboxes);

        if (nms)
            do_nms_sort(dets, nboxes, l.classes, nms);

        draw_detections(feed_img, dets, nboxes, thresh, names, alphabet, l.classes);
        draw_detections_mat(feed_img, feed, dets, nboxes, thresh, names, alphabet, l.classes);

        cv::imshow("test", feed);

        free_detections(dets, nboxes);

        int key = cv::waitKey(1);
        if (key == 'q')
        {
            break;
        }
        free_image(feed_img);
    }

    return 0;
}