Friday, July 4, 2014
Blog is Moving Location
While I have enjoyed using the Blogspot/Blogger platform, I have for a long time wanted a platform that would give me better control of my content and not make me worry about storage. The best option for me is a GitHub-based blog, so I have switched to using Octopress. You can now find me at http://garfieldnate.github.io. Hope to see you there!
Saturday, January 11, 2014
List Assignment in Scalar Context
(Cross-posted on blogs.perl.org)
This week I received some special help on SO in understanding how the goatse operator works. I was very thankful for everyone's help. These two articles were also very helpful and I recommend reading them.
Part of my confusion over the goatse operator was not knowing the difference between list and scalar assignment operators, which both are indicated via '='. Further confusing is the fact that each can be used in either scalar or list context, so you can have list assignment in scalar context or scalar assignment in list context.
The type of assignment is determined by what is being assigned to. As ikegami says, assignment to an aggregate is a list assignment, aggregate meaning an array, a hash, a parenthetical expression, or a my/our/local variable declared with parens.
The context of an assignment operator will really only matter when you are storing or checking the return value. You can store the value of an assignment operator by using another asignment operator: blah1 = blah2 = blah3, where blah1 is the value returned by assigning blah3 to blah2. The value gets checked in other contexts too, like inside a control structure condition:
I was pretty happy to finally understand this area I never quite understood I didn't understand (though someone might still point out I don't know what I'm talking about, as seems to be common with this subject). Today, though, I thought of one more usage of list assignment in scalar context that is probably used erroneously fairly often: quick and dirty parameter checking:
This week I received some special help on SO in understanding how the goatse operator works. I was very thankful for everyone's help. These two articles were also very helpful and I recommend reading them.
Part of my confusion over the goatse operator was not knowing the difference between list and scalar assignment operators, which both are indicated via '='. Further confusing is the fact that each can be used in either scalar or list context, so you can have list assignment in scalar context or scalar assignment in list context.
The type of assignment is determined by what is being assigned to. As ikegami says, assignment to an aggregate is a list assignment, aggregate meaning an array, a hash, a parenthetical expression, or a my/our/local variable declared with parens.
The context of an assignment operator will really only matter when you are storing or checking the return value. You can store the value of an assignment operator by using another asignment operator: blah1 = blah2 = blah3, where blah1 is the value returned by assigning blah3 to blah2. The value gets checked in other contexts too, like inside a control structure condition:
if(my $line = <>), etc. Here are examples for each combination of context and assignment operator:# scalar assignment in scalar context $thing = ($foo = 'bar'); # assignment returns $foo as lvalue say $thing; # bar # scalar assignment in list context ($thing) = ($foo = 'bar'); #assignment returns ($foo), $foo is lvalue say $thing; # bar # list assignment in scalar context; # assignment returns number of items in RHS of list assignment $thing = (($foo, $bar) = qw(foo bar)); say $thing; # 2 $thing = (() = qw(foo bar)) say $thing; # 2 $thing = () = qw(foo bar); say $thing; # 2 # list assignment in list context # assignment returns LHS list as lvalues ($thing) = (($foo, $bar) = qw(foo bar)); say $thing; # foo ($thing) = (() = qw(foo bar)); say $thing; # nothing ($thing is undef)That third one is of course the goatse operator. By the way for the record I totally think it looks more like a Saturn, though my wife disagrees and everyone seems to call it goatse. Anyway, though generally list assignment in scalar context is the rarest one, there are other occurrences. Ysth mentions the
each operator inside of a while loop:while(my ($key, $value) = each %hash)The aggregate on the left makes this list assignment, and
while makes it scalar context. Once the hash is out of keys, each returns () so that the assignment operator returns 0, finishing the while loop.I was pretty happy to finally understand this area I never quite understood I didn't understand (though someone might still point out I don't know what I'm talking about, as seems to be common with this subject). Today, though, I thought of one more usage of list assignment in scalar context that is probably used erroneously fairly often: quick and dirty parameter checking:
my ($input, $output) = @ARGV or die 'Usage: script <input> <output>';I always thought that the assignment would return $output, probably by analogy with comma expression assignment to a scalar (
$stuff = qw(foo bar)). However, if the user fails to provide a second parameter, the error would not be caught. This assignment will return the number of elements in @ARGV, which could be 1 instead of the required 2. So this use is only correct when unpacking @_ or @ARGV and expecting exactly one variable:my ($input) = @ARGV or die 'Usage: script <input>';This is probably obvious to Perl old-timers, but to me it was a revelation. And it doesn't look like I'm the only one, either. Grepping CPAN for assignment of an array to a parenthetical with 'or' after it turns up many mis-uses here.
Labels:
context,
list,
list assignment,
parameter checking,
Perl,
scalar,
scalar context
Tuesday, August 27, 2013
Packaging XML::LibXML with PAR Packer on Windows
PAR Packer is an excellent utility for delivering your Perl scripts as standalone executables. A standalone executable is highly desired in, for example, a corporate environment where everyone needs a program you wrote but you can't expect anyone to learn how to run Perl programs.
A recent requirement at $work was for a standalone executable. Originally, I was supposed to let my coworker work his magic (and his ActiveState PerlPacker license), but the client required an all-open-source solution. Thus I turned to PAR Packer and its pp utility.
So far, the most difficult aspect of using pp is that it doesn't detect all dependencies. It requires the user to explicitly list many required DLL's. I needed to list DLL's for two libraries: Wx and XML::LibXML.
Creating Wx apps with pp is a solved problem: wxpar, bundled with Wx::Perl::Packager, is a pp wrapper and adds all of the required Wx DLL's.
Getting it to work with XML::LibXML required some trial and error. I would create the executable, move it to another computer without Perl or C, run it from the command line (clicking the file hid certain error messages), and write down the name of the library that was missing. It turned out that three DLL's needed to be explicitly added: libxml2-2__.dll, libiconv-2__.dll and libz__.dll. On my computer these were located in C:\strawberry\c\bin. So, the final command I used to build my application was thus:
wxpar -o MyApp.exe -I lib -l C:/strawberry/c/bin/libxml2-2__.dll -l C:/strawberry/c/bin/libiconv-2__.dll -l C:/strawberry/c/bin/libz__.dll MyApp.pl
Is there a simpler way to do this? What's with all the underscores? Comments and questions welcome below.
A recent requirement at $work was for a standalone executable. Originally, I was supposed to let my coworker work his magic (and his ActiveState PerlPacker license), but the client required an all-open-source solution. Thus I turned to PAR Packer and its pp utility.
So far, the most difficult aspect of using pp is that it doesn't detect all dependencies. It requires the user to explicitly list many required DLL's. I needed to list DLL's for two libraries: Wx and XML::LibXML.
Creating Wx apps with pp is a solved problem: wxpar, bundled with Wx::Perl::Packager, is a pp wrapper and adds all of the required Wx DLL's.
Getting it to work with XML::LibXML required some trial and error. I would create the executable, move it to another computer without Perl or C, run it from the command line (clicking the file hid certain error messages), and write down the name of the library that was missing. It turned out that three DLL's needed to be explicitly added: libxml2-2__.dll, libiconv-2__.dll and libz__.dll. On my computer these were located in C:\strawberry\c\bin. So, the final command I used to build my application was thus:
wxpar -o MyApp.exe -I lib -l C:/strawberry/c/bin/libxml2-2__.dll -l C:/strawberry/c/bin/libiconv-2__.dll -l C:/strawberry/c/bin/libz__.dll MyApp.pl
Is there a simpler way to do this? What's with all the underscores? Comments and questions welcome below.
Labels:
DLL,
LibXML,
Par Packer,
Perl,
pp,
Windows,
wxpar,
WxWidgets,
XML,
XML::LibXML
Monday, April 15, 2013
The Extended Euclidian Algorithm in Perl
This week I learned about the extended Euclidian algorithm for finding a linear combination of two numbers that yields their GCD. For example, the GCD of 213 and 171 is 3, and -4*213 + 5*171 = 3. This algorithm is important in the RSA encryption scheme.
I had quite a difficult time getting myself to fully understand how it works. I jumped between Wikipedia, my data structures textbook (don't buy it), a YouTube video, and this excellent number theory class lecture.The lecture is the best, though I think there may be a typographical error in the recursive formula.
The basic idea uses recursion with an easy base step. We call Euclid(a,b) with a ≥ b:
To really help myself understand the whole thing, I wrote a Perl script to illustrate it. I put in lots of comments as I worked my way through it.
Feel free to leave a comment if you think that something could be stated more clearly. I hope it helps anyone else trying to learn how the extended Euclidian algorithm works.
I had quite a difficult time getting myself to fully understand how it works. I jumped between Wikipedia, my data structures textbook (don't buy it), a YouTube video, and this excellent number theory class lecture.The lecture is the best, though I think there may be a typographical error in the recursive formula.
The basic idea uses recursion with an easy base step. We call Euclid(a,b) with a ≥ b:
- The base case is when b is 0. The GCD of x and 0 is always x, and the coefficients to produce a GCD of 0 are 1 and 0 (or anything else): 1*x + 0(or anything)*0 = x. So the base case returns (1,0)
- Any other step starts by recursively calling Euclid(b, a mod b). We know that the GCD of a and b is the same as the GCD of b and a mod b (lemma 12 in the lecture). This recursive call is guaranteed to eventually get to the base case of b = 0.
- After finding the coefficients for producing the GCD from b and a mod b, we can calculate the ones for producing the GCD from a and b, because a mod b can be put in terms of a and b (see the code comments for the formulas).
To really help myself understand the whole thing, I wrote a Perl script to illustrate it. I put in lots of comments as I worked my way through it.
use strict;
use strict;
use warnings;
use 5.010;
#start with a >= b
my @nums = sort {$b <=> $a} @ARGV;
gcd(@nums);
#input: two numbers (a,b) a >= b > 0
#output: the coefficients which which yield their GCD;
sub gcd {
my ($a, $b) = @_;
#base case; the GCD of x and 0 always x;
#and the coefficients will always be 1 and 0 (or anything) because#1*x + 0*0 = xif($b == 0){ say "GCD is $a"; say "(a,b) = ($a,$b), coefficients = (1,0)"; say "1x$a + 0x$b = $a"; return (1, 0); } #otherwise, we evaluate u and v for k = ub + vr, where r is a mod b #gcd(b, a%b) gives the same value my $remainder = $a % $b; my ($u, $v) = gcd($b, $remainder); #now we can find k in terms of a and b because we know r in terms a and b #r = a - bq, where q = the whole part of a/b #k = ub + vr = ub + v(a - bq) = va + b(u-qv) #so the coefficient on a is v, and the coefficient on b is 1-qv my $x = $v; my $q = int(($a/$b)); my $y = $u - $q*$x; say "(a,b) = ($a,$b), coefficients are ($x,$y)"; say "${x}x$a + ${y}x$b = " . ($x*$a + $y*$b); return ($x, $y); }
Feel free to leave a comment if you think that something could be stated more clearly. I hope it helps anyone else trying to learn how the extended Euclidian algorithm works.
Sunday, April 7, 2013
Running Perl with Sublime Text 2
I've been having fun trying out Sublime Text. It's pretty, fast, and extremely extensible.
The first thing that I wanted was to be able to work well with Perl. I installed Package Control, followed by SublimeLinter, which has the perlcritic command built in. Making this useful requires a little finagling; perlcritic is by no means a quick program (being a really thorough linter for a language which is complex to parse), and the defaults for SublimeLinter cause it run over and over again as you type. To fix this, I edited Packages/SublimeLinter/SublimeLinter.sublime-settings and changed the "sublimelinter" setting to false. Now, in order to lint the current file, I have to press ctrl+alt+l. (Update: I don't recommend this for Sublime Text 2 because of speed problems. See this issue on Github. ST3 should be fine, though.)
Next, I wanted to be able to run my Perl scripts. Sublime has the ctrl+b shortcut for running a build for the current file. What the build actually does is specified in either a build file or the project file. To create a new build file for perl, go to Tools->Build System -> New Build System. The build file I've seen on different sites for Perl looks like this:
{ "cmd": ["perl", "$file"], "file_regex": ".* at (.*) line ([0-9]*)", "selector": "source.perl" }
Save this as perl.sublime-build. With this, whenever you are working on a Perl file and hit ctrl+b, the command "perl -w your_file.pl" will be run. This, however, was not good enough for me. Most of the time I am working on tests for a Perl module, so I have to run perl -Ilib t/my_test_file.t. I also want to be able to run individual tests as well as prove using shortcuts.
To do this, we need to turn the module directory into a Sublime Text project. This is pretty simple. First, open the module directory in Sublime Text. Select Project->Save Project As, then choose the name of the project and save it in the top directory of the module. Paste the following simple contents into the project file:
{ "folders": [ { "path": "." } ] }
Now I'd like to run my whole test suite using prove. By default, ctrl+shift+b runs a build variant with the name "Run", so we'll just make a prove variant with that name. I'd much rather give it a more descriptive name, but the Sublime shortcut requires this name. You can change the shortcut, but then you wouldn't be able to use the shortcut for other builds (other languages). It's all up to you. Here is the final build file:
The first thing that I wanted was to be able to work well with Perl. I installed Package Control, followed by SublimeLinter, which has the perlcritic command built in. Making this useful requires a little finagling; perlcritic is by no means a quick program (being a really thorough linter for a language which is complex to parse), and the defaults for SublimeLinter cause it run over and over again as you type. To fix this, I edited Packages/SublimeLinter/SublimeLinter.sublime-settings and changed the "sublimelinter" setting to false. Now, in order to lint the current file, I have to press ctrl+alt+l. (Update: I don't recommend this for Sublime Text 2 because of speed problems. See this issue on Github. ST3 should be fine, though.)
Next, I wanted to be able to run my Perl scripts. Sublime has the ctrl+b shortcut for running a build for the current file. What the build actually does is specified in either a build file or the project file. To create a new build file for perl, go to Tools->Build System -> New Build System. The build file I've seen on different sites for Perl looks like this:
{ "cmd": ["perl", "$file"], "file_regex": ".* at (.*) line ([0-9]*)", "selector": "source.perl" }
Save this as perl.sublime-build. With this, whenever you are working on a Perl file and hit ctrl+b, the command "perl -w your_file.pl" will be run. This, however, was not good enough for me. Most of the time I am working on tests for a Perl module, so I have to run perl -Ilib t/my_test_file.t. I also want to be able to run individual tests as well as prove using shortcuts.
To do this, we need to turn the module directory into a Sublime Text project. This is pretty simple. First, open the module directory in Sublime Text. Select Project->Save Project As, then choose the name of the project and save it in the top directory of the module. Paste the following simple contents into the project file:
{ "folders": [ { "path": "." } ] }
All this does is add the entire directory to the project. Next, we edit the Perl build file to reference the root of the project so we can add the top-level lib directory to our include path:
{
"cmd": ["perl", "-Ilib", "$file"],
"working_dir": "$project_path",
"file_regex": ".* at (.) line ([0-9])",
"selector": "source.perl",
}
Great! Now we can run Perl on tests contained in module directories. This still works fine for standalone scripts, too.
Now I'd like to run my whole test suite using prove. By default, ctrl+shift+b runs a build variant with the name "Run", so we'll just make a prove variant with that name. I'd much rather give it a more descriptive name, but the Sublime shortcut requires this name. You can change the shortcut, but then you wouldn't be able to use the shortcut for other builds (other languages). It's all up to you. Here is the final build file:
{
"cmd": ["perl", "-Ilib", "$file"],
"working_dir": "$project_path",
"file_regex": ".* at (.) line ([0-9])",
"selector": "source.perl",
"variants": [
{
"cmd": ["prove", "-vlr", "--merge"],
"working_dir": "$project_path",
"name": "Run",
"windows": {
"cmd": ["prove.bat", "-vlr", "--merge"]
}
}
]
}
Note that I needed a Windows variant for prove since the Sublime editor doesn't work the same as cmd. You could, alternatively, add '"shell":true' to use the system's command shell so you don't need a separate command for Windows.
With this build file in place, I can now press ctrl+b to run any Perl script, with it's project lib directory in @INC, and ctrl+shift+b to run prove. Voila!
Here are the final files:
project file (put a copy in your project root folders)
Perl build file (only one is needed per ST installation)
Here are the final files:
project file (put a copy in your project root folders)
Perl build file (only one is needed per ST installation)
Sunday, February 3, 2013
Managing Global State: the Flip-Flop Operator
Today I was faced with another mysterious failing test while writing a test suite for some legacy code. I knew it had to be a problem with persisting state because this particular test only failed when processing a particular data set with the same object which was just used to process another set.
My first step to trying to fix this was to delete all of the values stored in the object during the processing procedure:
Nothing changed. I reduced the problematic code into a small example for this post. First, the module to be tested:
The main idea here is that we are processing some file and returning a boolean representing its validity. The only requirement of validity of the file is that a certain start token is found within it; everything before the start sequence is ignored. Here are valid and invalid example files:
#good_file.txt
=startHere
hello
goodbye
#bad_file.txt- doesn't contain a start sequence
hello
goodbye
Now, the test file:
The output of running this file:
>perl test.pl
1..2
hello:)
goodbye:(
ok 1
hello:)
goodbye:(
not ok 2
# Failed test at test.pl line 61.
# Looks like you failed 1 test of 2.
Why did it fail the second test, which involves checking that an invalid file is considered invalid?
The bug is in the line which matches the start token:
With this, everything works as expected:
>perl test.pl
1..2
ok 1
File not processed; missing '=startHere' line.
ok 2
Note that this bug only presented itself to me because I changed the legacy standalone script to be its own module, creating the possibility of storing state between subroutine calls.
My first step to trying to fix this was to delete all of the values stored in the object during the processing procedure:
delete $self->{stateDatum1};
delete $self->{stateDatum2};
#etc....
Nothing changed. I reduced the problematic code into a small example for this post. First, the module to be tested:
package Demo::Bad::GlobalFlipFlop;
use strict;
use warnings;
use autodie;
use 5.010;
sub new {
my ($class) = @_;
my $self = {};
bless $self, $class;
return $self;
}
#return true if parsing succeeded, false otherwise.
sub parse {
my ($self, $file) = @_;
open my $file_in, '<', $file;
my $started = 0;
while( <$file_in> ){
#flip-flop
next unless /^=startHere/i .. 0; # start processing
$started = 1;
#continue doing something with file contents...
# say 'hello:)' if(/hello/);
# say 'goodbye:(' if(/goodbye/);
}
if(not $started){
say "File not processed; missing '=startHere' line.";
return;
}
close $file_in;
return 1;
}
1;
The main idea here is that we are processing some file and returning a boolean representing its validity. The only requirement of validity of the file is that a certain start token is found within it; everything before the start sequence is ignored. Here are valid and invalid example files:
#good_file.txt
=startHere
hello
goodbye
#bad_file.txt- doesn't contain a start sequence
hello
goodbye
Now, the test file:
use strict; use warnings; use autodie; use Test::More tests => 2; use File::Slurp; use Demo::Bad::GlobalFlipFlop; my $good_name = 'good_file.txt'; my $bad_file = 'bad_file.txt'; my $demo = Demo::Bad::GlobalFlipFlop->new(); ok( $demo->parse($good_name) ); ok( not $demo->parse($bad_file) );
The output of running this file:
>perl test.pl
1..2
hello:)
goodbye:(
ok 1
hello:)
goodbye:(
not ok 2
# Failed test at test.pl line 61.
# Looks like you failed 1 test of 2.
Why did it fail the second test, which involves checking that an invalid file is considered invalid?
The bug is in the line which matches the start token:
next unless /^=startHere/i .. 0; # start processingThe regex, flip-flop operator and 0 were clearly some sort of idiom that I was unfamiliar with. I had only ever used the flip-flop with numbers, such as 1..10, which iterates from numbers 1 through 10. How does it work? Let's check perlop:
Each ".." operator maintains its own boolean state, even across calls to a subroutine that contains it. It is false as long as its left operand is false. Once the left operand is true, the range operator stays true until the right operand is true, AFTER which the range operator becomes false again.The mysterious line thus worked like this:
- Skip lines of the input file until the left side, a match for the start token, is true
- Don't skip lines again until the right side, 0, is evaluated as true (which never happens).
- The state of this flip-flop operator is stored between subsequent calls to the subroutine. It's a hidden global variable!
my $started = 0;
while(<$file_in>){
if(/^=startHere/i){
$started = 1;
}
next unless $started;
#continue processing...
With this, everything works as expected:
>perl test.pl
1..2
ok 1
File not processed; missing '=startHere' line.
ok 2
Note that this bug only presented itself to me because I changed the legacy standalone script to be its own module, creating the possibility of storing state between subroutine calls.
Sunday, January 27, 2013
When not to use Perl's Implicit close; Suffering from Buffering
This post is a quick not on a bug I had difficulty tracking down.
One nice feature of Perl, introduced long before my time, is that of implicit closing. Perl closes filehandles for you when you forget (maybe on purpose). So the following is not a resource leak as a standalone script:
Today I found another case where not explicitly closing a filehandle means trouble. I was working on testing a modulino-style script with flexible outputs. You can call a method to set the handle that this script prints to. In my test script, I was setting the handle to be some filehandle and then checking the contents of the file against a string. The problem? The file was always empty at run time, but contained what I expected it to when I manually inspected it. Here's some example broken code:
Then, when you inspect the contents of file1.txt, you have:
some junk
What happened here? I was suffering from buffering. Because neither test.pl nor ImplicitClose.pm closed the file, it was still open when I was trying to read it. Nothing had been written to it yet because the amount printed was so small that it had to wait in the buffer either until there was more to write or until the file was closed, which would flush the buffer. Implicit close wouldn't be performed until the the filehandle's reference count reached 0, and the $demo object still had a reference to it. So the test would have worked fine if I had assigned undef to $demo, or just closed the filehandle.
Watch those implicit closes.
One nice feature of Perl, introduced long before my time, is that of implicit closing. Perl closes filehandles for you when you forget (maybe on purpose). So the following is not a resource leak as a standalone script:
open my $file, '>utf8', '/path/to/new/file' or die "couldn't open file: $!"; print $file 'Hello!';When the script finishes, Perl will close $file for you, so you can be nice and lazy. The caveat to this is that the variable $. isn't reset as it would be with a normal close (see docs here). $. holds the current line number from the last file read. So if you were processing a file line-by-line and found an error, you might print an error like 'bad value foo on line XYZ' using the $. variable for XYZ. I raised a question about this on StackOverflow.
Today I found another case where not explicitly closing a filehandle means trouble. I was working on testing a modulino-style script with flexible outputs. You can call a method to set the handle that this script prints to. In my test script, I was setting the handle to be some filehandle and then checking the contents of the file against a string. The problem? The file was always empty at run time, but contained what I expected it to when I manually inspected it. Here's some example broken code:
#ImplicitClose.pm
package Demo::Bad::ImplicitClose;
use strict;
use warnings;
sub new {
my ($class) = @_;
my $self = {};
bless $self, $class;
return $self;
}
sub output_fh {
my ( $self, $fh ) = @_;
if ($fh) {
if ( ref($fh) eq 'GLOB' ) {
$self->{output_fh} = $fh;
}
else {
open my $fh2, '>', $fh or die "Couldn't open $fh";
$self->{output_fh} = $fh2;
}
}
$self->{output_fh};
}
sub some_long_method {
my ($self, $text) = @_;
print { $self->{output_fh} } $text;
}
1;
#test.pl
use strict;
use warnings;
use autodie;
use Test::More tests => 1;
use File::Slurp;
use Demo::Bad::ImplicitClose;
my $file_name = 'file1.txt';
#make sure we pass the test from outputting something *this* run
unlink $file_name if -e $file_name;
my $print = 'some junk';
my $demo = Demo::Bad::ImplicitClose->new();
$demo->output_fh($file_name);
$demo->some_long_method($print);
my $contents = read_file($file_name);
is($contents, $print);
If you run test.pl, you'll see that its one and only test fails:
>perl -I[folder where you put the Demo directory] test.pl
1..1
not ok 1
# Failed test at test.pl line 68.
# got: ''
# expected: 'some junk'
# Looks like you failed 1 test of 1.
Then, when you inspect the contents of file1.txt, you have:
some junk
What happened here? I was suffering from buffering. Because neither test.pl nor ImplicitClose.pm closed the file, it was still open when I was trying to read it. Nothing had been written to it yet because the amount printed was so small that it had to wait in the buffer either until there was more to write or until the file was closed, which would flush the buffer. Implicit close wouldn't be performed until the the filehandle's reference count reached 0, and the $demo object still had a reference to it. So the test would have worked fine if I had assigned undef to $demo, or just closed the filehandle.
Watch those implicit closes.
Labels:
buffering,
filehandles,
Perl,
software,
special variables
Sunday, January 20, 2013
Testing Perl Distributions with Test Subdirectories
Normally I run my test suites with the prove utility:
prove -vl --merge
The v option turns on verbose processing, and the l option adds lib/ to the include path. prove then runs all of the tests in the t/ folder.
Today, I had a new problem. I merged multiple distributions into one (without losing any Git history!), and each had a test suite that I wanted to keep separate. Naturally, I moved the tests from each distribution into its own subdirectory under t/. However, this time when I ran prove -vl, I got this message:
Files=0, Tests=0, 0 wallclock secs ( 0.00 usr + 0.00 sys = 0.00 CPU)
Result: NOTESTS
prove -vl --merge
The v option turns on verbose processing, and the l option adds lib/ to the include path. prove then runs all of the tests in the t/ folder.
Today, I had a new problem. I merged multiple distributions into one (without losing any Git history!), and each had a test suite that I wanted to keep separate. Naturally, I moved the tests from each distribution into its own subdirectory under t/. However, this time when I ran prove -vl, I got this message:
Files=0, Tests=0, 0 wallclock secs ( 0.00 usr + 0.00 sys = 0.00 CPU)
Result: NOTESTS
Dubious... Well, I needed to know how to test with subdirectories in the t/ folder, so I looked at the prove documentation and found the -r option. The r stands for "recurse", meaning that the test files would be found by recursing into the directories of the distribution (starting at the top in the root of the distribution). That turned out to be exactly what I needed!
prove -vlr
t/parser/01-testParser.t
...
All tests successful.
Files=28, Tests=1815, 211 wallclock secs ( 0.92 usr + 0.28 sys = 1.20 CPU)
Result: PASS
Woohoo!
Also, both MakeMaker and Module::Build recurse in the same way during module testing. If you use Dist::Zilla, then you'll probably have the plugins [MakeMaker] and [ModuleBuild]. Using these, dzil test will recurse in the same way.
Labels:
Dist::Zilla,
distribution,
MakeMaker,
Module::Build,
Perl,
prove,
software,
subdirectories,
testing
Friday, August 17, 2012
Perl Tip: Don't Use a Makefile for Your Module
During my internship at SoarTech, I got a chance to learn a lot more about creating Perl modules. I put together a package of scripts for converting old file formats for speech recognition grammars, and I thought it worked beautifully. Of course, to start my module I used the classic tool h2xs:
My final code was well tested, and quick to install. I asked my co-worker to install it on his machine, and it was just as easy to use.
I was confident when I presented it to the company, until someone asked, "so, these Perl scripts, they work on Windows, Mac, Linux, etc., right?" I told them they should, since Perl is cross-platform. I became (a good kind of) paranoid, and asked my boss to test my code on his Mac (I was using Windows). The thing exploded when fed my script! I couldn't believe it! What could I have done so wrong?
So, the next day I stayed home a bit to borrow my wife's Mac and do more testing. But there was a big problem: my module used ExtUtils::MakeMaker to install itself. This has been the standard for years, and the majority of CPAN modules use this for installation. The cpan utility recognizes it, and runs installation automatically. However, MakeMaker is DOOMED! It requires an external tool, make, which you can find on every *nix platform, but everywhere else it has to be installed by the user. Strawberry Perl and ActiveState Perl for Windows come with a version (dmake or nmake). But on Mac, you have to install XCode, a whopping 4 gigabyte distribution for Mac developers.
...
Dangit...
My solution was to follow Michael Schwern's advice and convert to using Module::Build, which does not have external dependencies. There happens to be a converter to help you switch. When I ran it on my code, it didn't give a completely valid output, but the edits I did were minimal. From a user standpoint, the module will still be installed using the
When I put new distribution, with a shiny new Build.PL file, on a Mac, I still had some failed tests, but there weren't intermingled with the giant BOOM that happens when the
h2xs -X -n Foo::BarMy final code was well tested, and quick to install. I asked my co-worker to install it on his machine, and it was just as easy to use.
I was confident when I presented it to the company, until someone asked, "so, these Perl scripts, they work on Windows, Mac, Linux, etc., right?" I told them they should, since Perl is cross-platform. I became (a good kind of) paranoid, and asked my boss to test my code on his Mac (I was using Windows). The thing exploded when fed my script! I couldn't believe it! What could I have done so wrong?
So, the next day I stayed home a bit to borrow my wife's Mac and do more testing. But there was a big problem: my module used ExtUtils::MakeMaker to install itself. This has been the standard for years, and the majority of CPAN modules use this for installation. The cpan utility recognizes it, and runs installation automatically. However, MakeMaker is DOOMED! It requires an external tool, make, which you can find on every *nix platform, but everywhere else it has to be installed by the user. Strawberry Perl and ActiveState Perl for Windows come with a version (dmake or nmake). But on Mac, you have to install XCode, a whopping 4 gigabyte distribution for Mac developers.
...
Dangit...
My solution was to follow Michael Schwern's advice and convert to using Module::Build, which does not have external dependencies. There happens to be a converter to help you switch. When I ran it on my code, it didn't give a completely valid output, but the edits I did were minimal. From a user standpoint, the module will still be installed using the
cpan utility, so nothing has changed.When I put new distribution, with a shiny new Build.PL file, on a Mac, I still had some failed tests, but there weren't intermingled with the giant BOOM that happens when the
cpan utility can't find make. After fixing a bug or two, my module works on Windows and Mac and my boss is a happy camper.
Labels:
distribution,
MakeMaker,
Module::Build,
Perl,
software
Friday, June 29, 2012
Graphing grammar parses from CMU Sphinx 4
CMU Sphinx comes with some neat grammar parsing stuff that I never knew about. It uses JSGF (as detailed here) and comes with several demos, showing how to use a basic grammar, arc weights, tags, and even getting a javascript representation of the final parse! At work I've been needing to do some custom processing of the grammar output, but it was more conceptually difficult than I'd planned. So after figuring out how to traverse a parse tree, I decided to write a little application to print out the parse of a given sentence. The eclipse project for it can be found here.
Given this small gramamar:
#JSGF V1.0;
grammar sidTests ;
public <greet> = <greeting> [<person>] [i am <person>];
<greeting> = konnichiwa {language:japanese} | hello {language:english} | guten tag {language:german};
<person> = john {gender:man} | martha {gender:female} | kelly;
If we parse the sentence "konnichiwa kelly i am john", the program outputs the following:
digraph {
"greet-2147483647" [label="greet" color=magenta];
"greet-2147483647" -> "(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646";
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" [label="(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )" color=green];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "greeting-2147483645";
"greeting-2147483645" [label="greeting" color=magenta];
"greeting-2147483645" -> "konnichiwa {language:japanese}-2147483644";
"konnichiwa {language:japanese}-2147483644" [label="konnichiwa {language:japanese}" color=green];
"konnichiwa {language:japanese}-2147483644" -> "language:japanese-2147483643";
"language:japanese-2147483643" [label="{language:japanese}" color=red];
"language:japanese-2147483643" -> "konnichiwa-2147483642";
"konnichiwa-2147483642" [label="konnichiwa" color=cadetblue shape=box];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "(<sidTests.person> = kelly)-2147483641";
"(<sidTests.person> = kelly)-2147483641" [label="(<sidTests.person> = kelly)" color=green];
"(<sidTests.person> = kelly)-2147483641" -> "person-2147483640";
"person-2147483640" [label="person" color=magenta];
"person-2147483640" -> "kelly-2147483639";
"kelly-2147483639" [label="kelly" color=green];
"kelly-2147483639" -> "kelly-2147483638";
"kelly-2147483638" [label="kelly" color=cadetblue shape=box];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "i am (<sidTests.person> = john {gender:man})-2147483637";
"i am (<sidTests.person> = john {gender:man})-2147483637" [label="i am (<sidTests.person> = john {gender:man})" color=green];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "i-2147483636";
"i-2147483636" [label="i" color=cadetblue shape=box];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "am-2147483635";
"am-2147483635" [label="am" color=cadetblue shape=box];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "person-2147483634";
"person-2147483634" [label="person" color=magenta];
"person-2147483634" -> "john {gender:man}-2147483633";
"john {gender:man}-2147483633" [label="john {gender:man}" color=green];
"john {gender:man}-2147483633" -> "gender:man-2147483632";
"gender:man-2147483632" [label="{gender:man}" color=red];
"gender:man-2147483632" -> "john-2147483631";
"john-2147483631" [label="john" color=cadetblue shape=box];
}
which is all a big mess until we run it through GraphViz and see this:
A graph explaining how our sentence was parsed! I color code the parse: green is a RuleSequence, magenta is a RuleParse, light blue is a Token, red is a Tag, yellow (which there strangely aren't any of) is a RuleName.
I notice two very strange things here. First, RuleParses don't have anything as a direct child except for RuleSequences (RuleName is also possible but not shown). So RuleSequences will always be present and may only have one child. Second, text is treated as a sub-component of a tag instead of the other way around. So the text is tagging the tag? I don't know why they designed it that way, but at least now that I have a graph of the parse so I can figure out how to properly process it.
Given this small gramamar:
#JSGF V1.0;
grammar sidTests ;
public <greet> = <greeting> [<person>] [i am <person>];
<greeting> = konnichiwa {language:japanese} | hello {language:english} | guten tag {language:german};
<person> = john {gender:man} | martha {gender:female} | kelly;
If we parse the sentence "konnichiwa kelly i am john", the program outputs the following:
digraph {
"greet-2147483647" [label="greet" color=magenta];
"greet-2147483647" -> "(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646";
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" [label="(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )" color=green];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "greeting-2147483645";
"greeting-2147483645" [label="greeting" color=magenta];
"greeting-2147483645" -> "konnichiwa {language:japanese}-2147483644";
"konnichiwa {language:japanese}-2147483644" [label="konnichiwa {language:japanese}" color=green];
"konnichiwa {language:japanese}-2147483644" -> "language:japanese-2147483643";
"language:japanese-2147483643" [label="{language:japanese}" color=red];
"language:japanese-2147483643" -> "konnichiwa-2147483642";
"konnichiwa-2147483642" [label="konnichiwa" color=cadetblue shape=box];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "(<sidTests.person> = kelly)-2147483641";
"(<sidTests.person> = kelly)-2147483641" [label="(<sidTests.person> = kelly)" color=green];
"(<sidTests.person> = kelly)-2147483641" -> "person-2147483640";
"person-2147483640" [label="person" color=magenta];
"person-2147483640" -> "kelly-2147483639";
"kelly-2147483639" [label="kelly" color=green];
"kelly-2147483639" -> "kelly-2147483638";
"kelly-2147483638" [label="kelly" color=cadetblue shape=box];
"(<sidTests.greeting> = konnichiwa {language:japanese}) ( (<sidTests.person> = kelly) ) ( i am (<sidTests.person> = john {gender:man}) )-2147483646" -> "i am (<sidTests.person> = john {gender:man})-2147483637";
"i am (<sidTests.person> = john {gender:man})-2147483637" [label="i am (<sidTests.person> = john {gender:man})" color=green];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "i-2147483636";
"i-2147483636" [label="i" color=cadetblue shape=box];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "am-2147483635";
"am-2147483635" [label="am" color=cadetblue shape=box];
"i am (<sidTests.person> = john {gender:man})-2147483637" -> "person-2147483634";
"person-2147483634" [label="person" color=magenta];
"person-2147483634" -> "john {gender:man}-2147483633";
"john {gender:man}-2147483633" [label="john {gender:man}" color=green];
"john {gender:man}-2147483633" -> "gender:man-2147483632";
"gender:man-2147483632" [label="{gender:man}" color=red];
"gender:man-2147483632" -> "john-2147483631";
"john-2147483631" [label="john" color=cadetblue shape=box];
}
which is all a big mess until we run it through GraphViz and see this:
A graph explaining how our sentence was parsed! I color code the parse: green is a RuleSequence, magenta is a RuleParse, light blue is a Token, red is a Tag, yellow (which there strangely aren't any of) is a RuleName.
I notice two very strange things here. First, RuleParses don't have anything as a direct child except for RuleSequences (RuleName is also possible but not shown). So RuleSequences will always be present and may only have one child. Second, text is treated as a sub-component of a tag instead of the other way around. So the text is tagging the tag? I don't know why they designed it that way, but at least now that I have a graph of the parse so I can figure out how to properly process it.
Friday, May 11, 2012
Getting WordNet Verb Frames with JAWS
I love using JAWS to access WordNet. It has a rather extensive API, runs quickly, and doesn't require too much configuration. All you have to do is download the Jaws binary jar and WordNet, and then specify to JAWS where the WordNet files are (I will demonstrate this later).
One thing that did take a while to figure out was how to get verb frames from it. A verb frame is an indication of how the verb may be used. For example, the entry for the verb "fax" in WordNet contains the following frames:
02112546 39 v 04 sun 0 insolate 0 solarize 0 solarise ... 01 + 08 00 | expose to the rays of the sun or affect by exposure to the sun
00104147 29 v 02 sun 0 sunbathe ... 03 + 02 00 + 22 00 + 09 01 | expose one's body to the sun
The 01 and 03 indicate the number of verb frames, 08, 02, 22, and 09 are frame numbers. The 00's and 01 that follow the frame numbers indicate which words in the synset the numbers apply to. 00 means the frame is applicable to all members. The 01 in the second entry means that frame 9 is only for the word sun, and not for the second word, sunbathe.
There are two methods provided by JAWS to get frames. They are both contained in the VerbSynset class:
Keep in mind that the VerbSynset class is completely divorced from the actual orthographic representation of a word, since a synset may belong to several different words. The first method returns all of the frames that apply to every word in the synset, or to all of the frames marked with a 00 in the data.verb file as shown above. The second method returns only the frames which are marked as being specific to a single orthographic representation, specified by the one argument for the method. The return values are complementary and each is incomplete by itself. However, given only the synset offset or only the word to look up, JAWS is returning as much information as is possible. If you know both the synset number and the orthographic representation of a word you need frames for (and I don't see why you wouldn't), then the getWordFramesComplete method in the program below demonstrates how to get all of the available frames:
getWordFramesComplete calls both of the available methods in JAWS, retrieving both frames that apply to all words in a synset and the frames that are specific to a single word in the synset.
One thing that did take a while to figure out was how to get verb frames from it. A verb frame is an indication of how the verb may be used. For example, the entry for the verb "fax" in WordNet contains the following frames:
- Somebody ----s something to somebody
- Somebody ----s somebody something
- Somebody ----s somebody
- Somebody ----s something
- Somebody ----s
02112546 39 v 04 sun 0 insolate 0 solarize 0 solarise ... 01 + 08 00 | expose to the rays of the sun or affect by exposure to the sun
00104147 29 v 02 sun 0 sunbathe ... 03 + 02 00 + 22 00 + 09 01 | expose one's body to the sun
The 01 and 03 indicate the number of verb frames, 08, 02, 22, and 09 are frame numbers. The 00's and 01 that follow the frame numbers indicate which words in the synset the numbers apply to. 00 means the frame is applicable to all members. The 01 in the second entry means that frame 9 is only for the word sun, and not for the second word, sunbathe.
There are two methods provided by JAWS to get frames. They are both contained in the VerbSynset class:
/** * Returns the sentence frames (if any) associated with this verb meaning. * Sentence frames are examples of how the verb can be used / applied, and * all the frames returned by this method apply to all word forms in the * synset. * * @return Sentence frames associated with all word forms in this synset. * @see * Format of Lexicographer Files ("Verb Frames") */ public String[] getSentenceFrames(); /** * Returns the sentence frames (if any) that are specific to a particular * word form within this synset, where sentence frames are examples of * how the word form can be used / applied. * * @param wordForm Word form for which to return sentence frames. * @return Sentence frames that are specific to the word form. * @see * Format of Lexicographer Files ("Verb Frames") */ public String[] getSentenceFrames(String wordForm);
Keep in mind that the VerbSynset class is completely divorced from the actual orthographic representation of a word, since a synset may belong to several different words. The first method returns all of the frames that apply to every word in the synset, or to all of the frames marked with a 00 in the data.verb file as shown above. The second method returns only the frames which are marked as being specific to a single orthographic representation, specified by the one argument for the method. The return values are complementary and each is incomplete by itself. However, given only the synset offset or only the word to look up, JAWS is returning as much information as is possible. If you know both the synset number and the orthographic representation of a word you need frames for (and I don't see why you wouldn't), then the getWordFramesComplete method in the program below demonstrates how to get all of the available frames:
package edu.byu.xnlsoar.test;
import java.util.ArrayList;
import java.util.List;
import edu.smu.tspell.wordnet.Synset;
import edu.smu.tspell.wordnet.SynsetType;
import edu.smu.tspell.wordnet.VerbSynset;
import edu.smu.tspell.wordnet.WordNetDatabase;
import edu.smu.tspell.wordnet.impl.file.SampleFrameFactory;
import edu.smu.tspell.wordnet.impl.file.SynsetFactory;
import edu.smu.tspell.wordnet.impl.file.SynsetPointer;
public class DemoFrames {
private static WordNetDatabase database;
private static SynsetFactory synsetFactory;
//initialize everything here
static{
System.setProperty("wordnet.database.dir", "./lib/3.0/dict");
database = WordNetDatabase.getFileInstance();
synsetFactory = SynsetFactory.getInstance();
}
/**
*
* @param synsetOffset Synset number to look up frames for
* @return Array of frames for the synset; only returns frames
* which apply to every word in the synset
* frames
*/
public static List<string> getGeneralSynsetFrames(int synsetOffset){
SynsetPointer sp = new SynsetPointer(SynsetType.VERB, synsetOffset);
VerbSynset vSynset = (VerbSynset) synsetFactory.getSynset(sp);
List<string> frames = new ArrayList<string>();
for(String s : vSynset.getSentenceFrames())
frames.add(s);
return frames;
}
/**
*
* @param lemma Base form of the word you want to look up
* @return Array of frames for the lemma; only returns those
* that are specific to a particular word form within each synset.
* frames
*/
public static List<string> getWordFramesSpecific(String lemma){
List<string> frames = new ArrayList<string>();
Synset[] synsets = database.getSynsets(lemma,SynsetType.VERB);
for(Synset synset : synsets){
for(String s : ((VerbSynset) synset).getSentenceFrames(lemma))
frames.add(s);
}
return frames;
}
/**
* This one is more difficult to understand...
* @param lemma Base form of the word you want to look up
* @return Array of frames for the lemma; only returns those
* that match every word in each of the synsets that contain this word.
*/
public static List<string> getWordFramesGeneral(String lemma){
List<string> frames = new ArrayList<string>();
Synset[] synsets = database.getSynsets(lemma,SynsetType.VERB);
for(Synset synset : synsets){
for(String s : ((VerbSynset) synset).getSentenceFrames())
frames.add(s);
}
return frames;
}
/**
* This method is the best. It returns all possible frames
* given a synset number and the accompanying word.
* @param lemma Base form of the word you want to look up
* @param synsetOffset Synset number to look up frames for
* @return Array of frames for the synset; returns all frames
* for this word within this synset.
* frames
*/
public static List<string> getWordFramesComplete(String lemma, int synsetOffset){
SynsetPointer sp = new SynsetPointer(SynsetType.VERB, synsetOffset);
VerbSynset vSynset = (VerbSynset) synsetFactory.getSynset(sp);
List<string> frames = new ArrayList<string>();
for(String s : vSynset.getSentenceFrames(lemma))
frames.add(s);
for(String s : vSynset.getSentenceFrames())
frames.add(s);
return frames;
}
/**
* Prints out several different queries for the frames of "fax"
*/
public static void main(String[] args) {
int offset = 104147;//the synset meaning "expose one's body to the sun"
System.out.println(getGeneralSynsetFrames(offset));//returns 2 frames
System.out.println(getWordFramesSpecific("sun"));//returns 1 frame
System.out.println(getWordFramesGeneral("sun"));//returns 3 frames
System.out.println(getWordFramesComplete("sunbathe",offset));//returns 2 frames
System.out.println(getWordFramesComplete("sun",offset));//returns 3 frames (different from before)
}
}
getWordFramesComplete calls both of the available methods in JAWS, retrieving both frames that apply to all words in a synset and the frames that are specific to a single word in the synset.
Friday, December 30, 2011
Review: The Development of Language Processing Strategies: A Cross-linguistic Study Between Japanese and English
My rating: 4 of 5 stars
This is basically an updated version of Mazuka's PHD thesis. This book is a significant work on human sentence processing involving data from a head final and a head initial language.
Mazuka presents data on sentence processing experiments with English speaking adults and Japanese speaking children and adults. She shows that sentence processing strategies are the same in children and adults (though their ability differs with age), and that sentence processing strategies differ cross-linguistically. Her experimental data include probe latency tasks (PLTs) for lexical and semantic information in English and Japanese sentences.
A probe latency task involves a subject listening to a sentence and responding to questions about its contents. In lexical PLT, a subject is asked if the sentence contained the specified lexical item. In semantic PLT, the subject is asked if a sentence contained a portion which has a similar meaning to a specified word or phrase. The experimenter then carefully designs sentences which test the subjects' ability to process different types of sentences. Mazuka's experiment measure response time and also the accuracy of the subjects' responses. Her findings for cross-linguistic processing differences are as follows:
English speakers showed processing differences for main and subordinate clauses, while Japanese speakers did not.
English speakers showed different effects for semantic and lexical tasks, while Japanese speakers did not.
In English speakers, response time for semantic probe latency tasks involving sentence-initial subordinate clauses (an LB structure) was increased, while in Japanese speakers it was greatly decreased.
Japanese speaker response times to both lexical and semantic PLTs involving left-branching and coordinate structures were the same; English speakers showed much larger recency effects in LB than coordinate sentences.
Hypotheses about the human language processing mechanism which assume a single processing strategy do not account for these data. Japanese speakers process LB structures efficiently, and English speakers process RB structures efficiently. This is impossible in a parser which assumes only one processing strategy, and a parser which can efficiently process both would be too powerful to account for real human data. For Japanese speakers to process LB structures as efficiently as English speakers do RB structures, processing must be done bottom-up instead of top-down. Mazuka therefore hypothesizes that Universal Grammar (UG) contains a parameter which determines whether a language is right- or left-branching (RB or LB), and that this is linked with the processing strategy by specifying whether processing should be done top-down or bottom-up. She also hypothesizes that in English, main and subordinate clauses are processed to a different semantic level at some initial encoding stage, accounting differences in English main and subordinate clause PLT tasks. This needs to be further tested in the future with PLTs involving two clause sentences beginning with an explicit subordinator in Japanese.
She states that future research is required to determine the exact relationship between her experimental data and the operation of the human sentence parser as she has hypothesized.
Since some languages such as German, actually branch in different directions for different types of clauses, her hypothesis needs to be revised to account for this. I'm hoping that her hypotheses can be tested in detail in some sort of a cogntive modeling system.
View all my reviews
Labels:
Cognitive modeling,
English,
Japanese,
sentence processing
Sunday, November 6, 2011
List of Japanese NLP tools
I haven't tried out all of these so I don't have comments for everything, but hopefully this list will come in useful for someone.
Itadaki: a Japanese processing module for OpenOffice. I've done a tiny bit of work and issue documentation on a fork here, and someone forked that to work with a Japanese/German dictionary here.
GoSen: Uses sen as a base, and is part of Itadaki; a pure Java version of ChaSen. See my previous post on where to download it from.
MeCab: This page also contains a comparison of MeCab, ChaSen, JUMAN, and Kakasi.
ChaSen
JUMAN
Cabocha: Uses support vector machines for morphological and dependency structure analysis.
Gomoku
Igo
Kuromoji: Donated to Apache and used in Solr. Looks nice.
Hypermedia Corpus
TüBa-J/S: Japanese treebank from universityu of Tübingen. Not as heavily annotated as I'd hoped. You have to send them an agreement to download it, but it's free.
GSK: Not free, but very cheap.
LDC: Expensive unless your institution is a member
Kakasi: Gives readings for kanji compounds.
WordNet: Stil under development by NiCT. The sense numbers are cross-indexed with those in the English WordNet, so it could be useful for translation. Also, there are no verb frames like there are in English.
LCS Database: From Okayama University
Framenet: Unfortunately you can only do online browsing.
Chakoshi: Online collocation search engine.
Morphological analyzers/tokenizers
Corpora
Other lexical resources
Itadaki GoSen and IPADIC 2.7
Update3: I've forked the Itadaki project on GitHub to keep track of it better.
Update2: I made an executable JAR for GoSen that runs the ReadingProcessorDemo. It requires Java 6; just unzip the contents of this zip file to your computer and click on the jar file.
Update1: The IPADIC dictionary is no longer available from its original location. It has been replaced by the NAIST dictionary. I have edited the following post to reflect the needed changes.
Itadaki is a software suite for processing Japanese in OpenOffice. GoSen, part of the Itadaki project, is a pure Java morphological analysis tool for Japanese, and I have found it extremely useful in my research. Unfortunately, the page for this project went down recently, making the tools harder to find. Itadaki is still available through Google code here, but I can't find a separate installment of GoSen. The old GoSen website can still be accessed through the way-back-machine here. The other problem is that GoSen hasn't been updated since 2007, and in it's current release cannot handle the latest release of IPADIC. I'll describe how to fix it in this post.
Why does it matter that we can't use the latest version of IPADIC? Well, here's an example. I am using GoSen in my thesis work right now, and I put in a sentence which included a negative, past tense verb, such as 行かなかった. It analyzed it as な being used for negation, and かった being the past tense of the verb かう. That is indeed a problem! Using the newer IPADIC fixed it for me, though. To do that, download this modified version of GoSen. The explanation for the fix is here. Basically, a change in the new IPADIC versions to work better with MeCab adds a bunch of commas that break GoSen.
Edit: Once you've downloaded and unzipped GoSen, run ant in the top directory to build an executable JAR file. Note that if you want javadoc, you'll have to change build.xml so that the javadoc command has 'encoding="utf-8"'. Next, you must download the IPADIC dictionary from its legacy repository, here. Unpack the contents into testdata/dictionary. Change testdata/dictionary/build.xml so that the value of "ipadic.version" is "2.7.0" (the version that you downloaded). Now run ant in this directory to build the dictionary. [If you had errors, you may have forgotten to run ant in the top level directory first.]
Then, to run a demo and see what amazing things GoSen can do, copy the dictionary.xml file from the testdata/dictionary directory to the dictionary/dictionary directory, go back to the root directory of GoSen, and then run
Notice that it tokenizes the sentence, gives readings, and allows you to choose among alternatives analyses. It also gives information on part of speech and inflection.
To use GoSen in an Eclipse project, add gosen-1.0beta.jar to the project build path. You also need to have the dictionary directory somewhere, along with the dictionary.xml file. This code will get you started:
If you run that you will get:
You have plenty of other options while processing, like grabbing alternate readings, etc. Notice that it got one wrong here: ちゃう is a contraction of てしまう, not a verb whose lemma is ちゃう. It doesn't seem to work on contractions because every token needs a surface form. So this might not work well on informal registers such as tweets or blogs unless some pre-preprocessing is done.
Feel free to leave any questions or comments.
Update2: I made an executable JAR for GoSen that runs the ReadingProcessorDemo. It requires Java 6; just unzip the contents of this zip file to your computer and click on the jar file.
Update1: The IPADIC dictionary is no longer available from its original location. It has been replaced by the NAIST dictionary. I have edited the following post to reflect the needed changes.
Itadaki is a software suite for processing Japanese in OpenOffice. GoSen, part of the Itadaki project, is a pure Java morphological analysis tool for Japanese, and I have found it extremely useful in my research. Unfortunately, the page for this project went down recently, making the tools harder to find. Itadaki is still available through Google code here, but I can't find a separate installment of GoSen. The old GoSen website can still be accessed through the way-back-machine here. The other problem is that GoSen hasn't been updated since 2007, and in it's current release cannot handle the latest release of IPADIC. I'll describe how to fix it in this post.
Why does it matter that we can't use the latest version of IPADIC? Well, here's an example. I am using GoSen in my thesis work right now, and I put in a sentence which included a negative, past tense verb, such as 行かなかった. It analyzed it as な being used for negation, and かった being the past tense of the verb かう. That is indeed a problem! Using the newer IPADIC fixed it for me, though. To do that, download this modified version of GoSen. The explanation for the fix is here. Basically, a change in the new IPADIC versions to work better with MeCab adds a bunch of commas that break GoSen.
Edit: Once you've downloaded and unzipped GoSen, run ant in the top directory to build an executable JAR file. Note that if you want javadoc, you'll have to change build.xml so that the javadoc command has 'encoding="utf-8"'. Next, you must download the IPADIC dictionary from its legacy repository, here. Unpack the contents into testdata/dictionary. Change testdata/dictionary/build.xml so that the value of "ipadic.version" is "2.7.0" (the version that you downloaded). Now run ant in this directory to build the dictionary. [If you had errors, you may have forgotten to run ant in the top level directory first.]
Then, to run a demo and see what amazing things GoSen can do, copy the dictionary.xml file from the testdata/dictionary directory to the dictionary/dictionary directory, go back to the root directory of GoSen, and then run
java -cp bin examples.ReadingProcessorDemo testData/dictionary/dictionary.xml. The GoSen site says to run using the testdata folder, but that means you'll have to download the dictionary twice, which is dumb. When you run the above command, you'll get this GUI:Notice that it tokenizes the sentence, gives readings, and allows you to choose among alternatives analyses. It also gives information on part of speech and inflection.
To use GoSen in an Eclipse project, add gosen-1.0beta.jar to the project build path. You also need to have the dictionary directory somewhere, along with the dictionary.xml file. This code will get you started:
package edu.byu.xnlsoar.jp.lexacc;
import java.io.IOException;
import java.util.List;
import edu.byu.xnlsoar.utils.Constants;
import net.java.sen.SenFactory;
import net.java.sen.StringTagger;
import net.java.sen.dictionary.Morpheme;
import net.java.sen.dictionary.Token;
public class GoSenInterface {
public List tokenize(String sentence){
StringTagger tagger = SenFactory.getStringTagger(Constants.getProperty("GOSEN_DICT_CONFIG"));
try {
return tagger.analyze(sentence);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
return null;
}
public static void main(String[] args){
String sentence = "やっぱり日本語情報処理って簡単に出来ちゃうんだもんな。";
GoSenInterface dict = new GoSenInterface();
System.out.println("tokenizing " + sentence);
List tokens = dict.tokenize(sentence);
System.out.println(tokens);
Morpheme m;
System.out.println("surface, lemma, POS, conjugation");
for(Token t : tokens){
System.out.print(t + ", ");
m = t.getMorpheme();
System.out.print(m.getBasicForm() + ", ");
System.out.print(m.getPartOfSpeech() + ", ");
System.out.println(m.getConjugationalType());
}
}
}
If you run that you will get:
tokenizing やっぱり日本語情報処理って簡単に出来ちゃうんだもんな。
[やっぱり, 日本語, 情報処理, って, 簡単, に, 出来, ちゃう, ん, だ, もん, な, 。]
surface, lemma, POS, conjugation
やっぱり, やっぱり, 副詞-一般, *
日本語, 日本語, 名詞-一般, *
情報処理, 情報処理, 名詞-一般, *
って, って, 助詞-格助詞-連語, *
簡単, 簡単, 名詞-形容動詞語幹, *
に, に, 助詞-副詞化, *
出来, 出来る, 動詞-自立, 一段
ちゃう, ちゃう, 動詞-非自立, 五段・ワ行促音便
ん, ん, 名詞-非自立-一般, *
だ, だ, 助動詞, 特殊・ダ
もん, もん, 名詞-非自立-一般, *
な, だ, 助動詞, 特殊・ダ
。, 。, 記号-句点, *
You have plenty of other options while processing, like grabbing alternate readings, etc. Notice that it got one wrong here: ちゃう is a contraction of てしまう, not a verb whose lemma is ちゃう. It doesn't seem to work on contractions because every token needs a surface form. So this might not work well on informal registers such as tweets or blogs unless some pre-preprocessing is done.
Feel free to leave any questions or comments.
Thursday, November 3, 2011
CS 240 Web Crawler at BYU
I recently polished off the web crawler project for CS 240 at BYU. It's probably the most talked-about project in the CS major, and the cause of so many students retaking the class.
The specification for the web crawler assignment can be found here. Basically, given a start URL, the crawler finds every link on a page, follows them, downloads the pages, and indexes each of the words on a page, as long as they are not in a given stop words file; then it follows the links from that page, and so on. All of the indexed information is printed out to XML files. The code also has to conform to proper style, and no memory leaks are allowed.
For those who still need to do the project or haven't taken the following exam yet, I thought I'd post a note or two of help.
First off, check your constructors! In an initialization for a templatized BST node, I had been invoking the default copy constructor. A copy constructor looks like this:
In the contained object, I had only implemented the operator= construction. My class T had pointers in it, and those pointers were to objects which were allocated on the heap with the new keyword. The default copy constructor copied the pointers, and when the copy of the object of type T was deleted, so were the structures that its pointers pointed to. Since the original object pointed to the same structures, that object would then cause a segfault when destroyed because it would try to delete non-existent structures. Ouch!
That bug wasted a good 6 hours of my life. Needless to say, I was a little scared of the next assignment: a debugging exam. The class TAs put 4 bugs into our code (they didn't touch comments, asserts, or unit tests), and we had 3 hours to find them. Here's what the TA's did to my code:
In case somebody finds the code interesting/useful, I'll post it here (no cheating!). Make with
The specification for the web crawler assignment can be found here. Basically, given a start URL, the crawler finds every link on a page, follows them, downloads the pages, and indexes each of the words on a page, as long as they are not in a given stop words file; then it follows the links from that page, and so on. All of the indexed information is printed out to XML files. The code also has to conform to proper style, and no memory leaks are allowed.
For those who still need to do the project or haven't taken the following exam yet, I thought I'd post a note or two of help.
First off, check your constructors! In an initialization for a templatized BST node, I had been invoking the default copy constructor. A copy constructor looks like this:
T(const T & other)
In the contained object, I had only implemented the operator= construction. My class T had pointers in it, and those pointers were to objects which were allocated on the heap with the new keyword. The default copy constructor copied the pointers, and when the copy of the object of type T was deleted, so were the structures that its pointers pointed to. Since the original object pointed to the same structures, that object would then cause a segfault when destroyed because it would try to delete non-existent structures. Ouch!
That bug wasted a good 6 hours of my life. Needless to say, I was a little scared of the next assignment: a debugging exam. The class TAs put 4 bugs into our code (they didn't touch comments, asserts, or unit tests), and we had 3 hours to find them. Here's what the TA's did to my code:
- In my URL class, I call erase on a string representing a relative URL to get ride of the "../" at the beginning. The correct code is url.erase(0,3), but the TAs changed it to url.erase(0,2).
- In my BST Insert method, there is a control structure that determines whether to put a value on a node's left or right, and the TA's changed one of the left's to right's, i.e.
node->left = new BSTNode<T> (v);was changed tonode->right = new BSTNode<T> (v);. - I have several boolean flags in an HTMLparser class which keep track of whether processing is inside of a header, title, body, or html tag. They should all be false at the beginning of processing, but one of them was changed to true, e.g.
constructor():titleFlag(false),bodyFlag(false),headerFlag(true){... - The last bug was a memory leak. In my linked list Insert method, I declare a linked list node, use a control structure to determine the proper location of the new node, and then set the node with a call to
newand insert it in that location. The TA's changed the declaration to be a definition which used thenewkeyword, so I always allocated one extra node on the heap.
In case somebody finds the code interesting/useful, I'll post it here (no cheating!). Make with
make bin. Run with bin/crawler <start url> <stopwords file> <output file>.
Thursday, September 1, 2011
String Allignment with Edit Operations
A common way to measure the distance between two strings is using Levenshtein distance. Levenshtein distance is the minimum number of deletions, insertions, and substitutions needed to transform one string into another. Finding the distance between two strings is useful in certain applications such as spell checking (a word processor will suggest dictionary words that are close to your misspelled word). See wikipedia for more details and an example of Levenshtein distance calculation.
Another related and also important operation is to find the minimum edit alignment; that is, once the minimum edit distance between the two strings is found, output the sequence of operations that can be used to change the one string into the other. For example, if we let C mean "correct", S mean "substitution", D mean "deletion" and I mean "insertion", then the edit alignment between the characters in "construction" and "distortions" would be ISSCCISSCCCCD. Here is an explanation of the alignment:
Deletion: What
Substitution: My -> Your
Correct: house
Deletion: gleams
Correct: with
Correct: the
Correct: light
Correct: of
Correct: the
Insertion: the
Correct: moon
Correct: and
Substitution: your -> my
Correct: face
Feel free to use and edit this as you like. Many applications disregard the strings that are correct and only output the edit operations, and that should be an easy edit.
Another related and also important operation is to find the minimum edit alignment; that is, once the minimum edit distance between the two strings is found, output the sequence of operations that can be used to change the one string into the other. For example, if we let C mean "correct", S mean "substitution", D mean "deletion" and I mean "insertion", then the edit alignment between the characters in "construction" and "distortions" would be ISSCCISSCCCCD. Here is an explanation of the alignment:
- I: Insert a "c" ->cdistortions
- S: Substitute "d" for "o" ->coistortions
- S: Substitute "i" for "n" -> constortions
- CC: Leave the "st" alone
- I: Insert "r" -> constrortion
- S: Substitute "u" for "o" -> contrurtion
- S: Substitute "c" for "r" -> constructions
- CCCC: Leave "tion" alone
- D: delete "s" -> construction
/** * @return List of Operations representing allignment between list1 and * list2. The allignment represents operations to change list2 into list1. */ public static ListFor word level alignment, you can call it on Strings using the split function like so:levenshteinAllignment(Object[] list1, Object[] list2) { int[][] distanceMatrix = getDistanceMatrix(list1, list2); List ops= new ArrayList ( list1.length > list2.length ? list1.length : list2.length); //think of distance chart as going from bottom left to top right; //current position coordinates; start at top right. int row = list1.length; int col = list2.length; //could have moved to current position from three others; store their scores here. int diag; int left; int below; int current; while (row != 0 || col != 0) { diag = getVal(row-1,col-1,distanceMatrix); left = getVal(row,col-1,distanceMatrix); below = getVal(row-1,col,distanceMatrix); current = distanceMatrix[row][col]; // if the value in the diagonal cell (going up+left) is smaller or equal to the // values found in the other two cells // AND // if this is same or 1 minus the value of the current cell if(diag <= left && diag <= below && (diag == current || diag == current - 1)){ // then "take the diagonal cell" // if the value of the diagonal cell is one less than the current cell: if(diag == current - 1) // Add a SUBSTITUTION operation (from the letters corresponding to // the _current_ cell) ops.add(new Operation(Operation.Type.SUBSTITUTION,list1[row-1],list2[col-1])); else // otherwise: do not add an operation this was a no-operation. ops.add(new Operation(Operation.Type.CORRECT,list1[row-1])); //move diagonally row--; col--; } // // elseif the value in the cell to the left is smaller or equal to the value of // the cell below current cell // AND // if this value is same or 1 minus the value of the current cell else if(left < below && (left == current || left == current - 1)){ // add an INSERTION of the cell to the left ops.add(new Operation(Operation.Type.INSERTION,list2[col-1])); //move left col--; } // else else{ // take the cell below, add // Add a DELETION operation ops.add(new Operation(Operation.Type.DELETION,list1[row-1])); //move down row--; } } Collections.reverse(ops); return ops; } private static int getVal(int row, int col, int[][] distanceMatrix){ if(row < 0 || row > distanceMatrix.length) return Integer.MAX_VALUE; if(col < 0 || col > distanceMatrix.length) return Integer.MAX_VALUE; else return distanceMatrix[row][col]; } public static class Operation{ private Type type; private Object object1; private Object object2; public enum Type{ CORRECT,SUBSTITUTION,DELETION,INSERTION } public Operation(Type t, Object o1){ type = t; object1 = o1; object2 = null; } public Operation(Type t, Object o1, Object o2){ type = t; object1 = o1; object2 = o2; } @Override public String toString(){ if(type == Type.SUBSTITUTION) return "Substitution: " + object1.toString() + " -> " + object2.toString(); if(type == Type.CORRECT) return "Correct: " + object1.toString(); if(type == Type.DELETION) return "Deletion: " + object1.toString(); if(type == Type.INSERTION) return "Insertion: " + object1.toString(); return null; } } /** * * @param array of objects to compare * @param array of objects to compare * @return Levenshtein distance between arrays. * This method uses the equals(Object o) method to compare the * objects in the two arrays, returning the Levenshtein distance between them. */ public static int levenshteinDistance(Object[] list1, Object[] list2) { int[][] distance = getDistanceMatrix(list1, list2); return distance[list1.length][list2.length]; } /** * * @param list1 * @param list2 * @return A completely filled distance matrix; movement from [i-1][j] * represents insertion, from [i][j-1] represents deletion, and from * [i-1][j-1] represents substitution or no operation. */ private static int[][] getDistanceMatrix(Object[] list1, Object[] list2) { int[][] distanceMatrix = new int[list1.length + 1][list2.length + 1]; for (int i = 0; i <= list1.length; i++) distanceMatrix[i][0] = i; for (int j = 0; j <= list2.length; j++) distanceMatrix[0][j] = j; for (int i = 1; i <= list1.length; i++) for (int j = 1; j <= list2.length; j++) distanceMatrix[i][j] = minimum(distanceMatrix[i - 1][j] + 1,// insertion distanceMatrix[i][j - 1] + 1,// deletion distanceMatrix[i - 1][j - 1]// substitution or correct + ((list1[i - 1].equals(list2[j - 1])) ? 0 : 1)); return distanceMatrix; } /** * Same as Math.min, but returns the minimum of three arguments instead of * two. */ private static int minimum(int a, int b, int c) { return Math.min(Math.min(a, b), c); }
for(Operation o : levenshteinAllignment(
"What My house gleams with the light of the moon and your face"
.split(" "),
"Your house with the light of the the moon and my face"
.split(" "))
)
System.out.println(o);
And the output would be:Deletion: What
Substitution: My -> Your
Correct: house
Deletion: gleams
Correct: with
Correct: the
Correct: light
Correct: of
Correct: the
Insertion: the
Correct: moon
Correct: and
Substitution: your -> my
Correct: face
Feel free to use and edit this as you like. Many applications disregard the strings that are correct and only output the edit operations, and that should be an easy edit.
Wednesday, August 17, 2011
Distributional Statistics of Log Values in Linear Domain
Quite a long title, sorry. Basically, I had a bunch of log values (and they needed to stay logarithmic to avoid underflow) and I wanted to compute distributional statistics on them, like mean, variance and kurtosis. I wasn't sure if it would be valid to compute these kinds of statistics on the numbers as is, so I created a class to do all of the calculations in linear space. I'll post the result here, though there may be bugs (tell me if you find some!).
One thing to remember is that if the statistics map onto a negative number in linear space then it will be impossible to take the logarithm; therefore, these are invalid operations and you have to consider this before trying to retrieve any numbers from this program. I hope someone finds this useful!
One thing to remember is that if the statistics map onto a negative number in linear space then it will be impossible to take the logarithm; therefore, these are invalid operations and you have to consider this before trying to retrieve any numbers from this program. I hope someone finds this useful!
package edu.jhu.clsp.ws11.rerank.utils;
import java.util.Arrays;
/**
* This class returns distributional statistics given a list of numbers. The numbers are assumed to
* be in logarithmic space, and all of the computation is done on numbers converted from log to linear
* space; the results are returned again in log space.
* @author Nate Glenn
*
*/
public class LogDistributionalStats {
private double[] numbers;
private int N;//number of numbers input
private double logN;//log(N)
private double min;
private double median;
private double max;
private double mean;
private double avgAbsDeviation = 0;
private double standardDeviation = 0;
private double variance = 0;
private double skew = 0;
private double kurtosis = 0;
private double sum;
/**
* Compute statistics on nums. If norm is true, then compute statistics after normalizing
* the array, except for min, mean, and max.
*
*/
public LogDistributionalStats(double[] nums, boolean norm){
N = nums.length;
//must make new array so as to avoid overwriting the input.
numbers = new double[N];
for(int i = 0; i < numbers.length; i++)
numbers[i] = nums[i];
logN = Math.log(N);
//compute sum, mean, min, and max before normalization (if done at all)
sum = sumAsLinear();
mean = sum - logN;
Arrays.sort(numbers);
min = numbers[0];
max = numbers[N-1];
if(norm)
ArrayUtils.minusAll(numbers,sum);
double deviation;
if(N > 1){
for(double d : numbers){
deviation = LogMath.linearDifference(mean, d);
avgAbsDeviation = LogMath.addAsLinear(avgAbsDeviation, deviation);
variance += deviation*2;
skew += deviation*3;
kurtosis += deviation*4;
}
variance -= Math.log(N-1);
standardDeviation = variance/2;
skew -= logN+variance+standardDeviation;
//don't do negative 3 calculation here.
kurtosis = kurtosis-(logN + 2*variance);
}
else{
for(double d : numbers){
deviation = LogMath.linearDifference(mean, d);
avgAbsDeviation = LogMath.addAsLinear(avgAbsDeviation, deviation);
}
variance = Double.NaN;
standardDeviation = Double.NaN;
skew = Double.NaN;
kurtosis = Double.NaN;
}
avgAbsDeviation -= logN;
int mid = N/2;
if(N % 2 == 0)
median = LogMath.addAsLinear(numbers[mid-1], numbers[mid]) - Math.log(2);
else
median = numbers[mid];
}
/**
*
* @param nums
* @return Linear space sum of all numbers in nums
*/
private double sumAsLinear() {
double total = 0;
for(double d : numbers)
total = LogMath.addAsLinear(total, d);
return total;
}
public double getMin() {
return min;
}
public double getMax() {
return max;
}
public double getMean() {
return mean;
}
public double getStandardDeviation() {
return standardDeviation;
}
public double getVariance() {
return variance;
}
public double getSkew() {
return skew;
}
public double getSum() {
return sum;
}
/**
* Kurtosis is not calculated with any linear combinations (subtracting three)
* This is because it is often impossible to convert this to log space, since
* the final product is so often negative. If you want the minus three back again, you can
* try to minus it yourself and handle any exceptions (use LogMath.minusAsLinear()).
*/
public double getKurtosis() {
return kurtosis;
}
public double getMedian() {
return median;
}
}
Friday, July 29, 2011
Parsing in a Cognitive Modeller
A colleague of mine recently expressed interest in the research I am doing for an honors thesis. "Syntactic parsing in a cognitive modeling system" isn't exactly a crystal clear expression of my work, after all. I embarrassed myself by throwing out a few cursory statements before trailing off and ending with a "it's kind of hard to explain". Why can't I explain my own work? Partly because I don't explain it very often and therefore I'm bad at it. Another reason is that I have met with opposition to it in the past. Fellow linguists are immediately fascinated with the idea of modeling language use inside of a digital brain; computer scientists remain relatively unimpressed. "There are plenty of blazing fast parsers out there, so why build one just to act like a human? Besides, you don't even know if the computer is doing the same thing we are!" I'm going to use this and the next post to organize my thoughts and try to explain how parsing works in a cognitive modeling system, and why we would even try it in the first place.
This post will address cognitive modeling, and the next will move on to parsing.
Lets pretend that some crazy scientist manages to create an unstable time portal, like Will Robinson's, and that before getting to use it himself a rather valuable computer gets sucked through instead, sending it back to sometime in the 1950s. There, a curious electrical engineer finds it, and, realizing the novelty of it, shares his discovery with the scientists of his academic community. Though the origin of the machine is certainly not surmisable, they will try their darndest to figure out how it works.
They go about this by studying two aspects of the machine: the hardware and the software. When they look inside, they observe the mass of wires, chips, resistors, capacitors, and a myriad of other tiny gadgets all somehow integrated into one perfect system. They measure voltage across different points in the circuitry and observe the flow of power from the battery into the other parts. They analyze samples of it to discover what material it is made of, and run a whole bunch of other tests that are not entirely clear to non-engineers.
Observing the software, on the other hand, is much less complicated because all they have to do is turn it on. Let's just say Will Robinson's computer was a futuristic Mac of some sort. Then turning it on greets the scientists with a welcome screen and then Will's desktop:
The scientists are intrigued by the fact that the computer can do all sorts of complicated things, like play music and videos, compress files, typeset documents and manage large spreadsheets seemingly without doing any work. Putting it in perspective, their own computers look like this. They can surmise basically that there is one central system called OS-X that runs everything else, and that each of the functions run in separate programs. They learn that each of the programs run in a window, that programs are made up of more basic functions such as file management and that certain things are impossible, like creating files with "?" in the name.
They also tinker around with the hardware to see how each piece affects the software. Through this they uncover the difference between the RAM and a hard drive, the basic purpose of the CPU, and also that the USB ports transmit information.
After years of studying the Mac, they attempt to create machines which imitate its functions. One person creates a crude screen, another makes some memory with pathetic storage size, and others draft intricate blueprints explaining how the programs function within the machine. They don't all agree on the underlying mechanisms, so they split and pursue different theories. The end product is several schools of research attempting to build the machine by studying different aspects of it. Will they ever make an actual Macintosh themselves? Not likely, with 1950s technology. How can they even tell if they've gotten it right? They can experiment with their own machine and see if it acts somewhat like the Mac.
Now, what does all of this have to do with cognitive modeling?
Wait, what is cognitive modeling? To explain that, we first need a few definitions:
Data from these types of experiments contribute to our understanding of the mind, but we still do not completely understand the complex processes that make humans what they are. We can't try our hand at building a human, like the scientists did the Mac, either. Besides technological concerns, there are also ethical ones. Instead, scientists create computer models to simulate human activity. Although there have been many models which simulate single aspects of cognition such as hand-eye coordination or reading, general cognitive frameworks, which model human behavior overall, have also been created.
These computational cognitive models are extremely useful to researchers because they provide a universal testing ground in which mini theories about cognition can be tested. They form what Allen Newell called Unified Theories of Cognition (UTC). The name basically means that if a researcher has a theory of how one cognitive activity works, then it should fit within the larger, unified framework already tried and tested by the scientific community. Once the mini theory is implemented within the larger framework, experiments can be run using the resulting model, which is guaranteed to exhibit the properties of the larger framework. This has the benefit of constraining the variables in one's own model, making it both easier to design and scientifically more sound.
There are several other reasons that these models are useful:
This post will address cognitive modeling, and the next will move on to parsing.
Lets pretend that some crazy scientist manages to create an unstable time portal, like Will Robinson's, and that before getting to use it himself a rather valuable computer gets sucked through instead, sending it back to sometime in the 1950s. There, a curious electrical engineer finds it, and, realizing the novelty of it, shares his discovery with the scientists of his academic community. Though the origin of the machine is certainly not surmisable, they will try their darndest to figure out how it works.
They go about this by studying two aspects of the machine: the hardware and the software. When they look inside, they observe the mass of wires, chips, resistors, capacitors, and a myriad of other tiny gadgets all somehow integrated into one perfect system. They measure voltage across different points in the circuitry and observe the flow of power from the battery into the other parts. They analyze samples of it to discover what material it is made of, and run a whole bunch of other tests that are not entirely clear to non-engineers.
Observing the software, on the other hand, is much less complicated because all they have to do is turn it on. Let's just say Will Robinson's computer was a futuristic Mac of some sort. Then turning it on greets the scientists with a welcome screen and then Will's desktop:
The scientists are intrigued by the fact that the computer can do all sorts of complicated things, like play music and videos, compress files, typeset documents and manage large spreadsheets seemingly without doing any work. Putting it in perspective, their own computers look like this. They can surmise basically that there is one central system called OS-X that runs everything else, and that each of the functions run in separate programs. They learn that each of the programs run in a window, that programs are made up of more basic functions such as file management and that certain things are impossible, like creating files with "?" in the name.
They also tinker around with the hardware to see how each piece affects the software. Through this they uncover the difference between the RAM and a hard drive, the basic purpose of the CPU, and also that the USB ports transmit information.
After years of studying the Mac, they attempt to create machines which imitate its functions. One person creates a crude screen, another makes some memory with pathetic storage size, and others draft intricate blueprints explaining how the programs function within the machine. They don't all agree on the underlying mechanisms, so they split and pursue different theories. The end product is several schools of research attempting to build the machine by studying different aspects of it. Will they ever make an actual Macintosh themselves? Not likely, with 1950s technology. How can they even tell if they've gotten it right? They can experiment with their own machine and see if it acts somewhat like the Mac.
![]() |
| The scientists study the hardware and software inside of the futuristic Mac. |
Wait, what is cognitive modeling? To explain that, we first need a few definitions:
- Cognition: the excercise of human intelligence, including both deliberation and action, and performance of the wide variety of tasks that humans participate in.
- Cognitive Science: the study of minds as information processors, including how information is processed, represented, and transformed.
![]() |
| Cognitive scientists study and imitate the physical and behavioral aspects of the human mind. |
These computational cognitive models are extremely useful to researchers because they provide a universal testing ground in which mini theories about cognition can be tested. They form what Allen Newell called Unified Theories of Cognition (UTC). The name basically means that if a researcher has a theory of how one cognitive activity works, then it should fit within the larger, unified framework already tried and tested by the scientific community. Once the mini theory is implemented within the larger framework, experiments can be run using the resulting model, which is guaranteed to exhibit the properties of the larger framework. This has the benefit of constraining the variables in one's own model, making it both easier to design and scientifically more sound.
There are several other reasons that these models are useful:
- You don't have to pay the model to take your experiment, nor do you have to pay a technician to scan its brain while it carries out various tasks. Though initial programming and maintenance cost time and money, these models will carry out an infinite number of tasks for free.
- Because we can step through the execution of a program, we can see exactly what the model is "thinking" at all times (akin to "think aloud protocol"), allowing true introspection into the nature of the model and the theoretical consequences of its acceptance.
- Even if a model is worked out meticulously by hand, humans are error prone. Running the program on a computer guarantees accurate evaluation of the model.
- Models shared by the community provide baseline results, making work from different researchers comparable and making it easier to measure the advancement of the field.
- Models can be shared with other researchers easily via the internet.
There are several such available frameworks, including Soar, Allen Newell's creation, and ActR, which is more popular and seems to draw more government funding.
Like the 1950s scientists, researchers in cognition have split into different schools which study different aspects of the mind. The main split is between symbolic and subsymbolic models.
Here are some of the modeling projects that use a general cognitive framework:
A bunch using the COGENT framework; medical diagnoses, mental rotation, towers of Hanoi.
Learning to Play Mario using SOAR.
Pilot modeling using SOAR. Modeling pilot behavior for better mission planning and design.
Simulated students using ACT-R. The authors evaluate different instructional methods on simulated students.
Eagle Eye. Oops! That's not real. Maybe some day.
Language is an extremely complex phenomenon, but it too must be simulated in some way if we are to confirm the validity of our models. More on that next time.
Like the 1950s scientists, researchers in cognition have split into different schools which study different aspects of the mind. The main split is between symbolic and subsymbolic models.
- Symbolic models focus on the abstract symbol-processing capabilities of humans; we can combine physical patterns into structures and can also produce new expressions by manipulating other structures, e.g. art, language, music.
- Subsymbolic models focus on the neural properties of the brain; the most widely known is connectionism, which models complex behavior through connections between simple nodes.
Here are some of the modeling projects that use a general cognitive framework:
A bunch using the COGENT framework; medical diagnoses, mental rotation, towers of Hanoi.
Learning to Play Mario using SOAR.
Pilot modeling using SOAR. Modeling pilot behavior for better mission planning and design.
Simulated students using ACT-R. The authors evaluate different instructional methods on simulated students.
Eagle Eye. Oops! That's not real. Maybe some day.
Language is an extremely complex phenomenon, but it too must be simulated in some way if we are to confirm the validity of our models. More on that next time.
Subscribe to:
Posts (Atom)



