diff --git a/crf_data_gen/MednlpFeature.pm b/crf_data_gen/MednlpFeature.pm new file mode 100644 index 0000000..6847dee --- /dev/null +++ b/crf_data_gen/MednlpFeature.pm @@ -0,0 +1,70 @@ +#!/usr/bin/perl -w +package MednlpFeature; +use strict; +use Set::IntervalTree; +use Algorithm::AhoCorasick qw(find_all); +use base qw(Exporter); +our @EXPORT = qw( init_word_intervals bio_from_intervals init_dictionaries letter_type ); + +my @word_intervals; + +# search words in dictionaries and convert founds into interval +sub init_word_intervals { + my $input_joined = shift; + my $dictionaries = shift; + @word_intervals = map { + my $interval = Set::IntervalTree->new; + my $found = find_all($input_joined, @{$_}); + foreach my $pos (keys %$found) { + my $pos2 = $pos + length($found->{$pos}->[0]); + $interval->insert($pos, $pos, $pos2); + } + $interval; + } @$dictionaries; +} + +sub bio_from_intervals { + my $pos = shift; + my @bios = map { + my $interval = $_->fetch($pos, $pos + 1); + if (@$interval) { + ($interval->[0] == $pos) ? 'B' : 'I'; + } else { + 'O'; + } + } @word_intervals; + return join '', @bios; +} + +sub init_dictionaries { + my $dictionary_files = shift; + return map { + open DICT, $_ or die; + binmode DICT, ":encoding(utf-8)"; + my @dict = ; + close DICT; + map {chomp;} @dict; + \@dict; + } @$dictionary_files; +} + +sub letter_type { + my $str = shift; + if ($str =~ /^[\d0-9\.]+$/) { + return 'digit'; + } elsif ($str =~ /^\p{Latin}+$/) { + return 'latin'; + } elsif ($str =~ /^[\p{Hiragana}ー]+$/) { + return 'hiragana'; + } elsif ($str =~ /^[\p{Katakana}ー]+$/) { + return 'katakana'; + } elsif ($str =~ /^\p{Han}+$/) { + return 'kanji'; + } elsif ($str =~ /^\p{Common}+$/) { + return 'symbol'; + } else { + return 'mixed'; + } +} + +1; diff --git a/crf_data_gen/form_text.pl b/crf_data_gen/form_text.pl index 3c4f4a8..9c5d795 100755 --- a/crf_data_gen/form_text.pl +++ b/crf_data_gen/form_text.pl @@ -6,8 +6,8 @@ binmode STDOUT, ":encoding(utf-8)"; while () { - $_ =~ s/(\d+)-(\d+)-(\d+)/$1年$2月$3日/; - $_ =~ tr/[0-9a-zA-Z]/[0-9a-zA-Z]/; + s/[\x{0001}]//g; + s/(\d+)-(\d+)-(\d+)/$1年$2月$3日/; + tr/[0-9a-zA-Z%&#<>_\^\/\?\[\]]/[0-9a-zA-Z%&#<>_^/?[]]/; print $_; } - diff --git a/crf_data_gen/get_feature.pl b/crf_data_gen/get_feature.pl index 6d4a1b7..53c07d3 100755 --- a/crf_data_gen/get_feature.pl +++ b/crf_data_gen/get_feature.pl @@ -4,22 +4,16 @@ use Encode; use MeCab; use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat); -use Algorithm::AhoCorasick qw(find_all); -use Set::IntervalTree; +use FindBin; +use lib "$FindBin::Bin"; +use MednlpFeature; binmode STDOUT, ":encoding(utf-8)"; # read dictionaries my @dictionary_files; GetOptions ('d=s' => \@dictionary_files); -my @dictionaries = map { - open DICT, $_ or die; - binmode DICT, ":encoding(utf-8)"; - my @dict = ; - close DICT; - map {chomp;} @dict; - \@dict; -} @dictionary_files; +my @dictionaries = init_dictionaries(\@dictionary_files); # read input open INPUT, $ARGV[0] or die; @@ -27,17 +21,7 @@ my @input = ; my $input_joined = join '', @input; -# search words in dictionaries and convert founds into interval -my @word_intervals = map { - my $interval = Set::IntervalTree->new; - my $found = find_all($input_joined, @{$_}); - foreach my $pos (keys %$found) { - my $pos2 = $pos + length($found->{$pos}->[0]); - # print "word: $found->{$pos}->[0], $pos to $pos2\n"; - $interval->insert($pos, $pos, $pos2); - } - $interval; -} @dictionaries; +init_word_intervals($input_joined, \@dictionaries); my $pos = 0; # in input_joined @@ -55,17 +39,13 @@ my $surface = decode("utf-8", $node->{surface}); my $pos_temp = index($input_joined, $surface, $pos); # print "surface: $surface, pos_prev: $pos, pos_found: $pos_temp\n"; - my @bios = map { - my $interval = $_->fetch($pos_temp, $pos_temp + 1); - if (@$interval) { - ($interval->[0] == $pos_temp) ? 'B' : 'I'; - } else { - 'O'; - } - } @word_intervals; + my $bio_str = bio_from_intervals($pos_temp); + + my $letter_type = letter_type($surface); + + my $last_char = substr $surface, -1; - my $bio_str = join '', @bios; - my @out = map {(defined && $_ ne '') ? $_ : '*'} ($surface, $class1, $class2, $read, $bio_str); + my @out = map {(defined && $_ ne '') ? $_ : '*'} ($surface, $class1, $class2, $read, $bio_str, $letter_type, $last_char); my $out_str = join " ", @out; print $out_str, "\n"; diff --git a/crf_data_gen/parse.pl b/crf_data_gen/parse.pl index 0cae34e..44933c0 100755 --- a/crf_data_gen/parse.pl +++ b/crf_data_gen/parse.pl @@ -6,15 +6,16 @@ #see http://d.hatena.ne.jp/tagomoris/20120918/1347991165 for this configuration use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat); use Encode; -use Algorithm::AhoCorasick qw(find_all); -use Set::IntervalTree; use HTML::Entities; +use FindBin; +use lib "$FindBin::Bin"; +use MednlpFeature; binmode STDOUT, ":encoding(utf-8)"; if ($#ARGV < 0) {die "Usage: parse.pl [-tag t1,t2,...] [-notag t1,t2,...] [-attr] [-d dict]... input.xml\n";} # command line option handling -my @output_tags = ('a', 't', 'h', 'l', 'x', 'd', 'c', 'C', 'H', 'LOC', 'M', 'M1', 'T', 'T1', 'X'); # default tags to output +my @output_tags = ('mn', 'ms', 'm', 'a', 't', 'h', 'l', 'x', 'd', 'cn', 'cs', 'c', 'C', 'H', 'LOC', 'M', 'M1', 'T', 'T1', 'X'); # default tags to output my @no_output_tags = (); # exclusion my $to_use_modality = 0; @@ -34,7 +35,7 @@ init_output_tags(); my @dictionaries = init_dictionaries(\@dictionary_files); -my @word_intervals = init_word_intervals($text, \@dictionaries); +init_word_intervals($text, \@dictionaries); &traverse($doc); @@ -65,40 +66,14 @@ sub init_output_tags { ! $found} @output_tags; } -sub init_dictionaries { - my $dictionary_files = shift; - return map { - open DICT, $_ or die; - binmode DICT, ":encoding(utf-8)"; - my @dict = ; - close DICT; - map {chomp;} @dict; - \@dict; - } @$dictionary_files; -} - -# search words in dictionaries and convert founds into interval -sub init_word_intervals { - my $input_joined = shift; - my $dictionaries = shift; - return map { - my $interval = Set::IntervalTree->new; - my $found = find_all($input_joined, @{$_}); - foreach my $pos (keys %$found) { - my $pos2 = $pos + length($found->{$pos}->[0]); - $interval->insert($pos, $pos, $pos2); - } - $interval; - } @$dictionaries; -} - -sub print_iob_sequence { +sub iob_sequence_to_string { my $iobs_ref = shift; - foreach (@$iobs_ref) { + my @iob_strs = map { my @iob = @$_; @iob = map {(defined && $_ ne '') ? $_ : '*'} @iob; # avoid empty column - print ((join ' ', @iob)."\n"); - } + join ' ', @iob; + } (@$iobs_ref); + return join "\n", @iob_strs; } sub get_iob_sequence { @@ -117,18 +92,10 @@ sub get_iob_sequence { next if ($class1 =~ /BOS|EOS/); my $surface = decode("utf8", $node->{surface}); - - # generate feature from dictionary my $pos_temp = index($text, $surface, $pos_in_text); - my @iobs_from_dict = map { - my $interval = $_->fetch($pos_temp, $pos_temp + 1); - if (@$interval) { - ($interval->[0] == $pos_temp) ? 'B' : 'I'; - } else { - 'O'; - } - } @word_intervals; - my $iobs_from_dict_str = join '', @iobs_from_dict; + my $iobs_from_dict_str = bio_from_intervals($pos_temp); + my $letter_type = letter_type($surface); + my $last_char = substr $surface, -1; # forward position $pos_in_text = $pos_temp + length($surface); @@ -138,19 +105,10 @@ sub get_iob_sequence { my $tag = ($type eq 'O') ? 'O' : ($is_first ? "B-${type}" : "I-${type}"); $is_first = 0; - my @iob = ($surface, $class1, $class2, $read, $iobs_from_dict_str, $tag); + my @iob = ($surface, $class1, $class2, $read, $iobs_from_dict_str, $letter_type, $last_char, $tag); push @iobs, \@iob; } - # add a line break for each end of sentences - if (substr ($string, -1) eq "\n") { - my $iob_last = pop @iobs; - if (defined $iob_last) { - push @$iob_last, "\n"; - push @iobs, $iob_last; - } - } - return \@iobs; } @@ -178,16 +136,25 @@ sub print_leaf { my $node = shift; my $name = $node->nodeName(); - my $iobs; if (grep {$name =~ /^$_$/} @output_tags) { - $iobs = get_iob_sequence(get_modality($node), $node->textContent); + my $iobs = get_iob_sequence(get_modality($node), $node->textContent); + my $str = iob_sequence_to_string $iobs; + print "$str\n"; } else { my $parent_name = $node->parentNode()->nodeName(); unless ($node->hasChildNodes() || grep {$parent_name =~ /^$_$/} @output_tags) { - $iobs = get_iob_sequence('O', $node->textContent); + my $content = $node->textContent; + $content =~ s/^\s+//; + my @contents = split "\n+", $content; + my @strs = map { + my $iobs = get_iob_sequence('O', $_); + iob_sequence_to_string $iobs; + } @contents; + my $output = join "\n\n", @strs; + print $output; + print ($node->textContent =~ /\n$/ ? "\n\n" : "\n") if $output; } } - print_iob_sequence $iobs if defined $iobs; } sub traverse { diff --git a/evaluate/README b/evaluate/README index 293a5a9..d7b3ae3 100644 --- a/evaluate/README +++ b/evaluate/README @@ -1,17 +1,17 @@ - experiment.pl -usage: experiment.pl [-d dict]... [-d1 depth] [-d2 depth] [-mod] [-h] [-c file] -m model input +usage: experiment.pl [-d dict]... [-d1 depth] [-d2 depth] [-mod] [-c file] -m model input CRF++ のモデルファイルが得られている時, input に与えられたプレーンテキストにタグ付けして XML にする. モデルファイルは -m で与える (必須). -c で正解が与えられていれば conlleval.pl に渡せるように比較したファイルも生成する (cmp.txt). モデルの訓練時に辞書とのマッチの情報を使っている場合, 同じ辞書を -d で与える. --d1, -d2, -mod, -h は xml_to_charwise_iob.pl に渡される. +-d1, -d2, -mod は xml_to_charwise_iob.pl に渡される. - crfout_to_xml.pl -conlleval 形式のタグ付け結果から XML を生成する. +conlleval 形式のタグ付け結果から XML を生成する. ヘッダは付けないので valid な XML にはならないことに注意する. usage: crfout_to_xml.pl < (conlleval形式ファイル) @@ -21,7 +21,7 @@ IOB部分は'-'で分割され, 3番目の要素がある場合は modality の - xml_to_charwise_iob.pl -usage: xml_to_charwise_iob.pl [-d1 depth] [-d2 depth] [-mod] [-h] file1 [file2] +usage: xml_to_charwise_iob.pl [-d1 depth] [-d2 depth] [-mod] file1 [file2] XML からトークンを 1 文字ずつに取った conlleval 形式に変換する. 2 ファイルを入力することも出来, この場合 1 トークンに両方のタグの情報を付けて出力する. タグと空白を除いて一致しない場合はエラーを返す. @@ -34,5 +34,3 @@ y I-b となる. -mod は modality の値を出力するよう指定する. - --h は出力を人間に読みやすい形式にする. \ No newline at end of file diff --git a/normalizer/README b/normalizer/README new file mode 100644 index 0000000..9afbefd --- /dev/null +++ b/normalizer/README @@ -0,0 +1,34 @@ +- normalizer.pl + +Usage: normalizer.pl TAG DICT ATTR < INPUT + +入力 XML の TAG タグについてそのテキストを辞書 DICT を用いて正規化する. +結果は ATTR 属性に記録される. +正規化においては sim.pl が呼ばれる. sim.pl はこのスクリプトと同じディレクトリにある必要がある. + + +- sim.pl + +Usage: sim.pl -m table [-r "A/B"]... INPUT... +Example: sim.pl -m tables/Master_M.txt -r "XX/アク" アクチダス XXチダス ビルレクス + +入力単語を正規化する. +-m オプション(必須)は正規化テーブルを指定する. +-r オプション(複数指定可)は入力の部分的な変換を, 変換前と後をスラッシュで区切って指定する. +入力は複数指定可である. + + +- date_normalizer.pl + +Usage: date_normalizer.pl [-t tag] < INPUT + +時間を表すタグ(オプション t で指定可)を順に date2value.pl に入力し, 結果を absolute 属性に記録する. + + +- timeline.pl + +Usage: timeline.pl [-t tag] [-q] XML + +入力から各タグを切り出して時間タグが現れるところで分割した XML を返す. +-t tag は tag を時間を表したタグだと見なす(デフォルトは t). +-q は, 各イベントに対応するオリジナルのテキストを表示しないようにする. diff --git a/normalizer/normalizer.pl b/normalizer/normalizer.pl index 74240ca..952cd0c 100755 --- a/normalizer/normalizer.pl +++ b/normalizer/normalizer.pl @@ -10,59 +10,47 @@ binmode STDOUT, ":encoding(utf-8)"; binmode STDERR, ":encoding(utf-8)"; -if ($#ARGV < 0) {die "Usage: normalizer.pl [-tag tag=dict1[,dict2...]]... xml";} - -my %tag_to_table_files = (); -GetOptions('tag=s' => \%tag_to_table_files); - -die "Missing filename." if (!$ARGV[0]); +if ($#ARGV != 2) {die "Usage: normalizer.pl TAG DICT ATTR < INPUT";} my $parser = XML::LibXML->new(); -my $xml = $parser->parse_file($ARGV[0]); +my $in = join '', ; +my $xml = $parser->parse_string($in); my $doc = $xml->getDocumentElement(); -while (my ($tag, $table_files) = each %tag_to_table_files) { - normalize_tag($doc, $tag, $table_files); -} +normalize_tag($ARGV[0], $ARGV[1], $ARGV[2], $doc); print HTML::Entities::decode($doc->toString()); sub normalize_tag { - my ($doc, $tag, $table_files_text) = @_; + my ($tag, $dict, $attr, $doc) = @_; + print STDERR "normalize $tag -> $attr with $dict\n"; + my @nodes = $doc->getElementsByTagName($tag); my @texts = map {$_->textContent()} @nodes; my %unifier = map {$_, 1} @texts; # remove duplication @texts = keys %unifier; - my @table_files = split ',', $table_files_text; - my %normals; - - while (@texts && @table_files) { - my $query = join(' ', @texts); - $query =~ s/[\n\(\)\|;"'<>]//g; - next unless ($query); + my $query = join(' ', @texts); + $query =~ s/[\n\(\)\|;"'<>]//g; + return unless ($query); - my $table_file = shift @table_files; - print STDERR "normalize with $table_file\n"; - open(my $normal_in, "$Bin/sim.pl -m $table_file $query |"); - binmode $normal_in, ':encoding(utf8)'; + open(my $normal_in, "$Bin/sim.pl -m $dict $query |"); + binmode $normal_in, ':encoding(utf8)'; - while (<$normal_in>) { - chomp; - my @sp = split("\t"); - if (@sp > 1) { - $normals{$sp[0]} = $sp[1]; - print STDERR "$sp[0] -> $sp[1]\n"; - } + my %normals; + while (<$normal_in>) { + chomp; + my @sp = split("\t"); + if (@sp > 1) { + $normals{$sp[0]} = $sp[1]; + print STDERR "$sp[0] -> $sp[1]\n"; } - - @texts = grep {! (exists $normals{$_})} @texts; # filter out if normal form found } foreach (@nodes) { my $normal = $normals{$_->textContent()}; - $_->setAttribute('normal', $normal) if (defined $normal); + $_->setAttribute($attr, $normal) if (defined $normal); } } diff --git a/normalizer/sim.pl b/normalizer/sim.pl index eb46dc0..80ab08b 100755 --- a/normalizer/sim.pl +++ b/normalizer/sim.pl @@ -11,7 +11,7 @@ binmode(STDIN, ":encoding(utf8)"); binmode(STDOUT, ":utf8"); -# ./sim.pl "遊離コレステロール" Master_T.txt "F/遊離" +# ./sim.pl -m tables/Master_M.txt -r "XX/アク" アクチダス XXチダス ビルレクス foreach (@ARGV) {$_ = decode('utf8', $_);} my $master_file; @@ -54,7 +54,7 @@ sub search { } } } - return $max_codes[0]; + return (@max_codes < 20) ? (join ',', @max_codes) : ''; } sub read_rules { @@ -90,9 +90,10 @@ sub paraphrase { my @n = split(/,/, $rule->[0]); foreach my $tmp (@n) { next unless ($input =~ /$tmp/); - if ($tmp =~ /^\w+$/ && $input =~ /(.*)$tmp(.*)/) { + my $w = "0-9A-z"; + if ($tmp =~ /^$w+$/ && $input =~ /(.*)$tmp(.*)/) { # アルファベットの部分一致を無視する - next if (($1 =~ /\w$/) or ($2 =~ /^\w/)); + next if (($1 =~ /$w$/) or ($2 =~ /^$w/)); } # パラフレーズを表示 # print "change[".$input."]".$tmp."-->".$rule->[1]."\n"; @@ -137,9 +138,10 @@ sub get_sim { } if ($sim == 0) { # STEP2: 構成要素の部分一致をみる foreach (@n2) { - if (length($tmp) >= 1 && $tmp =~ /^\w+$/ && $_ =~ /(.*)$tmp(.*)/) { + my $w = "0-9A-z"; + if (length($tmp) >= 1 && $tmp =~ /^$w+$/ && $_ =~ /(.*)$tmp(.*)/) { # アルファベットの部分一致を無視する - next if ($1 =~ /\w$/) or ($2 =~ /^\w/); + next if ($1 =~ /$w$/) or ($2 =~ /^$w/); $sim = length($tmp) / 2; #print "partly match [". $tmp2."]-[".$tmp."]".$1."\n"; } diff --git a/normalizer/timeline.pl b/normalizer/timeline.pl index e415d0b..957a385 100755 --- a/normalizer/timeline.pl +++ b/normalizer/timeline.pl @@ -26,22 +26,32 @@ sub convert_tag { my $node = shift; my $name = $node->nodeName; if ($name =~ /[cC]/) { - $node->setNodeName('treatment'); - } elsif ($name =~ /[mM]/) { $node->setNodeName('symptom'); + } elsif ($name =~ /[mM]/) { + $node->setNodeName('treatnment'); } else { $node->setNodeName('condition'); } } +sub copy_attributes { + my $source = shift; + my $dist = shift; + map { + /\s*(.+)="(.+)"/ or die "malformed attribute: $_"; + $dist->setAttribute($1, $2); + } ($source->attributes()); +} + sub proc_node { - my ($text_acc_ref, $tag_acc_ref, $time_ref, $node) = @_; + my ($text_acc_ref, $tag_acc_ref, $time_node_ref, $node) = @_; if ($node->nodeName eq $time_tag) { my $text = join '', @$text_acc_ref; if ($text !~ /^\s*$/) { # event my $event = $timeline->createElement('event'); - $event->setAttribute('date', $$time_ref); + $event->setAttribute('date', $$time_node_ref->textContent()); + copy_attributes($$time_node_ref, $event); map {$event->appendChild($_)} @$tag_acc_ref; # text if (!$to_deter_original_text) { @@ -55,7 +65,7 @@ sub proc_node { } @$tag_acc_ref = (); @$text_acc_ref = (); - $$time_ref = $node->textContent; + $$time_node_ref = $node; } elsif ($node->nodeName eq '#text') { push @$text_acc_ref, $node->nodeValue; } elsif ($node->nodeName eq 'medtexts') { @@ -73,10 +83,10 @@ sub traverse { my @text_acc = (); my @tag_acc = (); - my $time = 'top'; + my $time_node; while (1) { - proc_node (\@text_acc, \@tag_acc, \$time, $node); + proc_node (\@text_acc, \@tag_acc, \$time_node, $node); # move node if ($node->hasChildNodes()) { # go down diff --git a/templates/default b/templates/default index 530bbe7..c7f6487 100644 --- a/templates/default +++ b/templates/default @@ -29,5 +29,17 @@ U72:%x[0,4] U73:%x[1,4] U74:%x[2,4] +U80:%x[-2,5] +U81:%x[-1,5] +U82:%x[0,5] +U83:%x[1,5] +U84:%x[2,5] + +U90:%x[-2,6] +U91:%x[-1,6] +U92:%x[0,6] +U93:%x[1,6] +U94:%x[2,6] + # Bigram B diff --git a/templates/rich b/templates/rich index a318dd1..1b28da0 100644 --- a/templates/rich +++ b/templates/rich @@ -1,4 +1,6 @@ # Unigram + +# surface U000:%x[-2,0] U001:%x[-1,0] U002:%x[0,0] @@ -14,7 +16,10 @@ U009:%x[-2,0]/%x[-1,0]/%x[0,0] U010:%x[-1,0]/%x[0,0]/%x[1,0] U011:%x[0,0]/%x[1,0]/%x[2,0] +U012:%x[-3,0] +U013:%x[3,0] +# POS U100:%x[-2,1] U101:%x[-1,1] U102:%x[0,1] @@ -30,7 +35,10 @@ U109:%x[-2,1]/%x[-1,1]/%x[0,1] U110:%x[-1,1]/%x[0,1]/%x[1,1] U111:%x[0,1]/%x[1,1]/%x[2,1] +U112:%x[-3,1] +U113:%x[3,1] +# POS detail U200:%x[-2,2] U201:%x[-1,2] U202:%x[0,2] @@ -46,6 +54,9 @@ U209:%x[-2,2]/%x[-1,2]/%x[0,2] U210:%x[-1,2]/%x[0,2]/%x[1,2] U211:%x[0,2]/%x[1,2]/%x[2,2] +U212:%x[-3,2] +U213:%x[3,2] + # Yomi U300:%x[-2,3] U301:%x[-1,3] @@ -58,7 +69,14 @@ U306:%x[-1,3]/%x[0,3] U307:%x[0,3]/%x[1,3] U308:%x[1,3]/%x[2,3] +U309:%x[-2,3]/%x[-1,3]/%x[0,3] +U310:%x[-1,3]/%x[0,3]/%x[1,3] +U311:%x[0,3]/%x[1,3]/%x[2,3] +U112:%x[-3,3] +U113:%x[3,3] + +# dictionaries U400:%x[-2,4] U401:%x[-1,4] U402:%x[0,4] @@ -74,6 +92,48 @@ U409:%x[-2,4]/%x[-1,4]/%x[0,4] U410:%x[-1,4]/%x[0,4]/%x[1,4] U411:%x[0,4]/%x[1,4]/%x[2,4] +U112:%x[-3,4] +U113:%x[3,4] + +# type of charcters +U500:%x[-2,5] +U501:%x[-1,5] +U502:%x[0,5] +U503:%x[1,5] +U505:%x[2,5] + +U505:%x[-2,5]/%x[-1,5] +U506:%x[-1,5]/%x[0,5] +U507:%x[0,5]/%x[1,5] +U508:%x[1,5]/%x[2,5] + +U509:%x[-2,5]/%x[-1,5]/%x[0,5] +U510:%x[-1,5]/%x[0,5]/%x[1,5] +U511:%x[0,5]/%x[1,5]/%x[2,5] + +U112:%x[-3,5] +U115:%x[3,5] + +# last character in surface +U600:%x[-2,6] +U601:%x[-1,6] +U602:%x[0,6] +U603:%x[1,6] +U606:%x[2,6] + +U605:%x[-2,6]/%x[-1,6] +U606:%x[-1,6]/%x[0,6] +U607:%x[0,6]/%x[1,6] +U608:%x[1,6]/%x[2,6] + +U609:%x[-2,6]/%x[-1,6]/%x[0,6] +U610:%x[-1,6]/%x[0,6]/%x[1,6] +U611:%x[0,6]/%x[1,6]/%x[2,6] + +U612:%x[-3,6] +U613:%x[3,6] + # Bigram B +