Japomatik http://rogerbraun.net Most recent posts at Japomatik posterous.com Tue, 21 Feb 2012 06:01:00 -0800 A client-side Bayes classifier for Hacker News http://rogerbraun.net/a-client-side-bayes-classifier-for-hacker-new http://rogerbraun.net/a-client-side-bayes-classifier-for-hacker-new

Hacking Hacker News, now in JS!

Do you like reading Hacker News? A lot of people do, but they usually get the same problem after a while: There are just too many stories submitted each day.

If you really want to read them all, you will spend a large amount of your copious free time reading stuff you don't actually care about. Joel Grus, a future famous author, had the same problem and found a solution: Training a naive Bayes classifier to recognize interesting and boring stories.

If you haven't already, you should really take a look at his post, complete with code. Here is what he does in a nutshell:

  1. Grab new story submission data from the unofficial HN API.
  2. Rate the submissions by classifying them using only this data: Title, link, submitter.
  3. Sort the stories by interestingness.
  4. Post them to a blog.

Voila, you know have your own HN RSS feed, sorted by interestingness.

I really like the idea of adding some kind of automatic filtering to a news site. Maybe my tastes changed or HN did, but I just don't find most of the stories posted there to be that interesting anymore.

Instead of reading the RSS feed, I usually just read the HN homepage. This will of course filter out a lot of boring stories already. But there are still many stories I don't want to read that do show up. I don't want to read stuff about Occupy on HN, I already read BoingBoing. I also don't need too many stories about Apple, I can just read Folklore.org.

So, being the copycat that I am, I adapted Joel's idea to my habits: I also wrote a naive Bayes classifier, a way to train it and to rate stories. There is one rather big difference between Joel's version and mine, though: This one is a JS bookmarklet that augments the HN homepage, running completely in your browser.

Adding a classifier to the HN homepage

Writing a naive Bayes classifier is quite easy, and you can probably do it in a day or two. I wrote a rather generic one in JS, the code is on Github. Don't look too hard at the code, I am a JS novice. If you clone the code and try the demo page, you can train a small classifier yourself and play with it a bit.

Writing the algorithmic part is easy, the complicated part is actually packaging it into a useful bookmarklet:

  • How do I get the text data for training and rating?
  • How do I preserve this data across browser sessions?
  • How do I control this from the HN homepage?

Luckily, these things are all very doable in today's Web 2.0 world.

To get the data in, I use the very handy ViewText, which will reduce any web site to it's main text content. Think of it as Readability with an API. I use JSONP to retrieve the content of a story and then use this for training or rating. The results will still contain some markup, but as this is evenly distributed among interesting and boring stories it should not affect the rating.

The training data is saved as a simple hash with this structure: {"apple": {"interesting": 5, "boring": 2}}. This means that in all the documents used for training, the word "apple" appeared 5 times in interesting ones and 2 times in boring ones. So if you now find the word "apple" in a new document, the document is probably interesting.

This hash (and a bit of additional info) is saved in the browser using DOM Storage, which is a bit like cookies on steroids. I just read the saved hash into memory when the bookmarklet is executed and save the hash after every additional training. It's a simple solution and I guess it will come in handy more often.

Integration with the HN homepage is also not that hard: Just add some icons after every story for training as good, training as a bad and giving the current rating. This is easy with jQuery and did not take me too long.

And that's pretty much it! If you want to try it yourself, just drag the HNBayes bookmarklet to your bookmarks bar, visit HN and click on it. You should now have the additional buttons for training and rating. Rate some stories as good or bad by clicking thumbs up or thumbs down, then see what the classifier thinks by clicking on the eye.

UPDATE: It seems that Posterous will not let me post links with javascript inside them. Nice. While I figure this out, just copy this into a bookmark:

javascript:(function(){var script = document.createElement("script"); script.src="https://raw.github.com/rogerbraun/HNBayes/master/hnbayes.js"; document.body.appendChild(script);})();

Amjcuxrcmaal3rc

If you want to see the code, look here.

Final remarks

It's wonderful what one can do today with some of the newer additions to browsers, like DOM storage. Things that I previously thought were pretty much impossible client-side now seem to be almost trivial. JS has really come a long way since the days of alert() and document.write().

The concept of rating links automatically is, of course, not restricted to HN, but could be extended for any other site. I like that, with this solution, you can have a personalized experience while still not giving anyone your data. On the other hand, your ratings are locked away in your browser: Right now, there is no way to sync it except copying the stored hashes.

Some pages will not rate or train with this bookmarklet. This is because ViewText can not read them and will give an invalid response. I have seen this happen with stories on WSJ, HN itself and with PDFs. I don't really plan to fix this, as most sites will work just fine, but who knows...

If you like or dislike this approach (and my execution of it), you may want to leave a comment! I'd like to hear your thoughts.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Fri, 10 Feb 2012 16:25:27 -0800 How to do a JSONP request http://rogerbraun.net/how-to-do-a-jsonp-request http://rogerbraun.net/how-to-do-a-jsonp-request

Some of my friends are learning Javascript this year, thanks to the great folks at Codeyear. Of course, the one thing everybody wants to do is put random dinosaur images on every page. But how can you do that?

Searching for random images

You probably know Google Image Search, or Bing Image Search. They can deliver somewhat random images for pretty much anything you'd want. Just search for something and get a picture - nice!

Now you all know how to search via the usual HTML interface, but these services often also have an API. For example, here is the documentation for Googles Image Search API. It's deprecated, so don't use it for anything serious, but it should be fine for us.

So what's so special about using an API? The nice thing about it is that it gives us just what we need and nothing more: A Javascript object with all the information we want - picture url, picture caption, thumbnail... We don't need all the other stuff that is only there for humans to look at. But how do we access this information with an API?

Accessing the Google API is done via HTTP GET, so you can just copy the API-URIs into your browser and you will see the returned Javascript object. The API has a base URI - https://ajax.googleapis.com/ajax/services/search/images - and takes at least two parameters: v, which is the version number of the API and 1.0 in our case. And q, which is the query string - the keywords you want to search for.

So an URL like https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=apple will return the first set of results for a search for the keyword "apple". Take a look at the response in the browser, and you will recognize the familiar Javascript Object Notation. Or JSON, as we will call it from now on. Now let's use JS to get this data.

Ajax

If you know a bit about the Interweb 2.0, you probably have heard of the term "Ajax", which means "Asynchronous Javascript and XML". Luckily, it has really nothing to do with XML and does not have to be asynchronous. We can also use it to get JSON data - much easier to work with! Essentially, an XMLHttpRequest object can do everything your browser does: GET data, POST data, with or without parameters, whatever you want. It's an easy way to access data from other web pages from your JS without changing the whole page.

Right now you are surely thinking: "So I just use this XMLHttpRequest object, make the request to my API and use the response in my Javascript code". That's the right idea, but there is a small detail that will prevent this... XMLHttpRequests are subject to the same origin policy. What this means is that, essentially, you can only use it when requesting something from your own domain. Well, I guess you don't host your stuff on googleapis.com, so there seems to be no way to do a request to the API from Javascript. But of course, there is.

JSONP

No, there are parts of HTML which predate the same origin policy. And luckily, the script tag is one of them. You may have noticed that you can use js files from anywhere in the web just fine in your script tags. This is often used to load some kind of standard js, like jQuery, from someone else's server. But now you know the dark secret: You can include ANY javascript from ANYWHERE in the web with a script tag. Just build a script tag with document.createElement("script"), point it to some js file and append it to your document. It will execute just fine.

Now we know how to execute any random Javascript from somewhere else. But how can we use this to retrieve data? We don't want to execute anything, we just want our search results! Now here comes the clever part. Let's say our API usually returns this object:

{caption: "Cute Cat", url: "http://cutecats.com/cat.jpg}

So we have a hash with a caption and an URL. This is a valid Javascript object, but that does not help us. We want that object, but we want it as a parameter to a function we define that does all kinds of stuff with it. So if our function was called "addPictures", we would want something like this:

addPictures({caption: "Cute Cat", url: "http://cutecats.com/cat.jpg});

This would give our self-defined function the response data we want. And that's exactly what a JSONP request does. The Google API takes another optional paramater, "callback". This defines the name of the function that will be wrapped around the response object. Now your inserted script tag will run the remote Javascript, which will call your locally defined function to handle the response.

You can now circumvent the same origin policy. It's wrong, I know. But it feels so right.

Some words about the name

So why is this technique called JSONP? Well, it's supposed to mean "JSON with padding". This is such a bad name. It's not padded, it's wrapped in a function call! And it's not even JSON! It could actually be any kind of Javascript. It should rather be called something like "script tag request insertion" or whatever. I know I am complaining too much, but why give such a misleading name to this technique? Whatever. Now you know what it really does.

An example: Doing an Image Search

Look at this gist that does an image search with Google's API and shows the first four result images.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
  <head>
    <title>JSON-P Example</title>
    <script src="jsonp.js"></script>
  </head>
  <body>
    <h1>A simple JSON-P example</h1>
    <p>This is a Google Image Search.</p>
    <input id="q" />
    <button id="search">Search</button>
    <div id="images" />
  </body>
</html>

Now, this uses bare Javascript. You might want to use something like jQuery or Ender in your real projects, but this is for educational purposes.

Take a look at the doJsonP function. This actually inserts the script tag and appends it to the document body, so it will get the remote JS and execute it. It also binds the callback function to the global variable "jsoncallback" so it can later use this function name for the JSON wrapper.

So, when you call doJsonP(someUrl, someFunction), your function "someFunction" will be called with the response of the request to someUrl. In our callback function we just append the four result images to our document so we can see that everything works fine. But you could, of course, do anything you can imagine.

That's it. Now you know everything you need to know about JSONP to finally write your "Dinosaurify" bookmarklet. When you are done, be sure to post a link the comments!

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Mon, 07 Nov 2011 15:15:00 -0800 Simple testing for your algorithmic Javascript http://rogerbraun.net/simple-testing-for-your-algorithmic-javascrip http://rogerbraun.net/simple-testing-for-your-algorithmic-javascrip

Here's the backstory.

I noticed that Codebrawl does not yet support Github Flavored Markdown in Gist comments. Gist comments get loaded client-side via the Gist API and are then rendered with showdown.js. Showdown is a great project, but it does not support the extensions Github uses. This really sucks for gist comments which contain code, as they are rendered as regular Markdown-style code spans, which ignores newlines and indentation and makes them completely unreadable.

So I forked the project, added the necessary 30 lines of code and made a pull request. I "tested" my code the stupid way: Look at the output in the browser. This made me feel somewhat uneasy. I want this code to be useful, but how should anyone know that I did not break anything? I decided to add some tests. I have done testing in other languages, but never in Javascript. How do you do this?

Testing your Javascript

How can you test Javascript? JS usually runs in the browser, so you could just make an html page that runs your test code. The downside is that you need a browser and you need to refresh it. I'd rather work only on the command line for this project, as my JS does not really do anything that needs a DOM. It just translates one string to another. So what about testing it in node.js or something?

There is a really nice looking BDD testing framework for Javascript that also supports node.js: Jasmine by Pivotal Labs. It looks pretty much like what I want. It has an RSpec-like syntax and supports all kinds of run options. Having said that, I could not get it to run at all, at least not in under an hour. This was probably my own fault. Still, it drove me to think about what I actually needed and if I could just write it myself.

I really just want two things for this: RSpec-like syntax and RSpec-like output. I like the descriptive "it should do somethign", but I don't need all this beautiful "things.should be_wonderful" stuff, a simple assert_equal would be enough. 

Here is what I came up with.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var assert_equal = function(a, b) {
  if(a === b){
    return {passed: true};
  } else {
    throw { message: ("Expected:\n\"" + a + "\"\nGot:\n\"" + b + "\""), passed: false}
;
  }
}

var indent = function(text) {
  return " " + text.replace(/\n/g, "\n ");
}

var it = function(name, code) {
  var res = {passed: true}
  try {
    code();
  } catch(er) {
    res = er;
  }

  if(res.passed){
    console.log("\033[1;32mPassed: " + name + "\033[0m");
    return true;
  } else {
    console.log("\033[1;31mFailed: " + name + "\n\033[0m");
    console.log(indent(res.message));
    return false;
  }
}

This allows you to write tests like this:

1
2
3
4
5
6
7
8
9
10
11
it("should make paragraphs out of simple texts", function() {
  var text = "Hallo";
  var html = converter.makeHtml(text);
  var expected = "<p>Hallo</p>"
  assert_equal(html, expected);

  text = "Hello\n\nHow are you?";
  html = converter.makeHtml(text);
  expected = "<p>Hello</p>\n\n<p>How are you?</p>"
  assert_equal(html, expected);
});

Looks kinda okay! Now to run the tests, just run the specs through node.js. Here is a screenshot.

Jstestsuccess

Nice! How does it look if it fails?

Jstestfail

Also nice! I think I'll use this for now.

This is of course not a full featured solution. Right now, it only has an assert_equal method to test things - you could easily add more methods, though. I found that this mini-framework works quite well for my DOM-less algorithmic javascript, without introducing any external dependencies. If you have a similar problem, you might want to check out my showdown fork at https://github.com/rogerbraun/showdown and see how I use this.

Have fun!

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Tue, 27 Sep 2011 18:56:59 -0700 Selective color effect with ChunkyPNG, or: How I won Codebrawl #10 http://rogerbraun.net/selective-color-effect-with-chunkypng-or-how http://rogerbraun.net/selective-color-effect-with-chunkypng-or-how

The selective color effect, implemented with ChunkyPNG

So, I won Codebrawl #10. I participated the previous time that ChunkyPNG was featured and I really enjoyed using it. ChunkyPNG is a pure Ruby library for handling PNG files. One of the nice features it has is that it gives you direct read-write access to the pixels, so you can just load an image, do a map or fold over the array of pixels and then save it right back to a new PNG. ChunkyPNG is well documented and gives you several easy methods to work with pixels and colors, so be sure to take a look

When I started to work on this Codebrawl, I thought about two ways to do this: Try to detect each crayon as an object and keep one of them in color, or take a reference color and just keep the colors that are similar to it. Object detection would be great, of course. Just changing colors based on reference does not work in cases were you want to, for example, keep a flag in color. Okay, it does work when you use it on a Japanese flag... Just using a color reference and a distance does work great for the example image that the Codebrawl uses, though, and implementing object detection is a much harder problem than mapping over colors.

I think I know how to iterate over an array, so all I was missing was a distance function for colors. But how do you actually compare colors?

How to measure the distance between colors

There are several ways to get a value that tells you how much alike two colors are. If you have an RGB pixel, you could just take the red, green and blue values as coordinates in a three dimensional space and calculate the distance there (and I guess you know how to do that).

I did not actually implement this, as it does not represent similar colors in the way we would see them. Think about it, the color (255, 0, 0) and (127, 0, 0) are both just shades of red, but they would have the same distance as (255, 0, 0) and (255, 128, 0), which introduces green to the mix and looks pretty different overall.

This also tells us something about what kind of representation we want: One where only the color counts, but not the brightness (or luminance). Skipping through Wikipedia, I first noticed rg chromacity space.

rg chromacity

rg chromacity is a simple way to remove intensity information from your colors and only keep the proportions of red, green and blue. You normalize the values to be between 0 and 1, with r + g + b always adding up to one. It is called rg chromacity because only the red and green components are needed to describe a color, as the blue component is always b = 1 - (r + g). For example, rgb(255,0,0) is rg(1,0), as is rgb(127,0,0).

You now have a 2d space, so distances can be easily calculated. Using this to decide which colors to keep gives somewhat satisfactory results, at least for some of the crayons. But in the end, it just was not as good as I had hoped. Part of this surely is the way this color space actually looks. It is somewhat uneven, as the distance between pure red, rg(1,0), and pure blue, rg(0,0), is one. But between red and green, rg(0,1), it is the square root of two!

By just measuring the distance, I am essentially cutting a circle of colors I want to keep out of this triangle, but colors in this space are not uniformly distributed. I had a lot of ideas how to counter these problems: Make colors spread from a reference color if the neighbors are similar, make negative cuts in this space by specifying colors that should always be made gray, etc... What I really needed was a better distance function for my colors. So let's use a grown-ups color system, HSV.

HSV

If you go looking for color spaces, you will encounter HSL and HSV pretty soon. They use hue, saturation and lightness (or value, respectively) to define a color. If you take a look at some pictures of this color space, you will see that the hue component looks pretty much like what we need.

Now, calculating the hue is more complicated than calculating rg chromacity colors. If I understood it correctly, you make a hexagon of colors, put red at 0°, green at 120° and blue at 240° and then calculate where your color lies. You can see the formula I used in the code.

Calculating the distance is just subtracting one hue from the other. We have to do it two times, though, as the hue is circular and has red on both ends, so the shorter distance counts. This gives good results for most colors. I could not get the yellow and red crayon to seperate perfectly... This may be because I did not find a good reference color, or just that the dark yellow in the tip of the crayon and the light red are actually too similar and this approach won't work at all. You should take a look at the other entries and see how well they did.

Better color distance

I tackled the problem of color similarity from a rather primitive point of view: Numerical values of single pixels. As it turns out, this is not enough to mirror human color perception. Take a look at this: http://gizmodo.com/5839481/the-most-wicked-optical-illusion-ive-seen-so-far. Both spirals have the same color, but you would never think that if you didn't know or check. As far as I know, no algorithm that works for cases like this exists, yet.

There is a standard for measuring color difference by the International Commission on Illumination (sounds good, right?). It takes into account how humans perceive color, so it should give you the best results (if you are human, that is). I did not try it, though, as it seems somewhat complicated and I wanted to keep this entry short and to the point. Peplin did it like this and his code is much more complicated, but still quite readable. One problem with this approach is that you have a lot of magic numbers that you get from the standard definition. These are (at least for me) hard to visualize and I find it hard to grasp their meaning.

Anyway, here are my pictures and my Ruby code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module ChunkyPNG::Color
  # See http://en.wikipedia.org/wiki/Hue#Computing_hue_from_RGB
  def self.hue(pixel)
    r, g, b = r(pixel), g(pixel), b(pixel)
    return 0 if r == b and b == g
    ((180 / Math::PI * Math.atan2((2 * r) - g - b, Math.sqrt(3) * (g - b))) - 90) % 360
  end

  # The modular distance, as the hue is circular
  def self.distance(pixel, poxel)
    hue_pixel, hue_poxel = hue(pixel), hue(poxel)
    [(hue_pixel - hue_poxel) % 360, (hue_poxel - hue_pixel) % 360].min
  end
end

module SelectiveColor
  # Really simple, just change the pixels to grayscale if their distance to a
  # reference hue is larger than a delta value.
  def to_selective_color!(reference, delta)
    pixels.map!{|pixel| ChunkyPNG::Color.distance(pixel, reference) > delta ? ChunkyPNG::Color.to_grayscale(pixel) : pixel}
    self
  end
end

BONUS: Coffeescript version

I also wrote a Coffescript version of the same algorithm which you can use to quickly check the effects of changing the reference color or the color distance. Porting it over from Ruby was pretty straight-forward, except for the strange interface for pixels that the HTML5 canvas element uses and the fact that modulus is broken in Javascript (WTF?!). I tried to take a functional approach, but I am not a Coffee/Javascript programmer, so this code is rather bad. If you are interested anyway, you can see the code at GitHub.

You can find this version ready to use at http://severe-autumn-9391.heroku.com/index.html. Use the slider to set the color distance that will still be colored, and just click on any point in the image on the right to set it as the reference color. Have fun!

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sun, 11 Sep 2011 18:51:00 -0700 Tuning your Maxixe segmenter with the new optimizer http://rogerbraun.net/tuning-your-maxixe-segmenter-with-the-new-opt http://rogerbraun.net/tuning-your-maxixe-segmenter-with-the-new-opt

Remember when we talked about Maxixe? We talked about how to use it to train a very simple segmenter and split a sentence without spaces into single words. But now you will probably want to try it yourself, for real, in your big enterprisey next big thing! But how do you actually do that?

Ingredients

Maxixe needs three things to build a segmenter:

  1. A large corpus of text data. This should be as close to your production data as possible.
  2. A set of n-gram counts from this corpus. For example, all 2-grams, 3-grams and 4-grams from the corpus are counted and then saved as the index.
  3. A threshold that controls how lhigh the score for a possible segmentation has to be for it to be cut. Simply put, the lower this is, the more segments you will get.

The corpus

So how do you get hundres of megabytes of text in your target language? This can actually be pretty easy! For example, Wikipedia has dumps of their database for all languages, and some other gals and guys already figured out how to get a plain text corpus from them. This can be a bit much and you will probably want to try only a subset of Wikipedia. Also remember: After using Wikipedia as a corpus, your segmenter will be trained to split encyclopedia articles, but it will not necessarily be well equipped to split blog comments or 4chan memes. 

Another source can be Project Gutenberg, which has a lot of free books in plain text format, ripe for the taking. Similar pages are available for more specialized areas. For japanese, there is Aozora Bunko, for classical japanese you can take a look at the Japanese Text Intiative. Just type "$favorite_language text" in Google or DDG and use whatever comes up. Okay, if you need Chinese, I searched for that, too.

Anyway, you now have your corpus. AWSM! What to do next?

Building your index

When you use the segmenter, you can actually change the threshold value anytime you like (you should not, though). But you will have to decide in advance which n-gram count indexes you want to have, as these have to be pre-computed. So which n-grams should you index?

I know that every time I write "n-gram", someone stops reading. What is this thing? An n-gram is just a string of length n. So let's say we have the string "nyan" and want all 2-grams that are in it. Easy! Just start at the beginning and take two characters each time. So for "nyan" we get ["ny", "ya", "an"]. The 3-grams would be ["nya", "yan"]. 

Now, remember that the Tango algorithm that Maxixe uses depends on the fact that n-grams that are part of words or words themselves are more likely to occur in a corpus than n-grams that are not. A simple example: If you have a large corpus, your 3-gram count for "cat" and "dog" will probably be rather high, but the count for "qxc" will be rather low, as a word would need to end with "qx", begin with "xc" or even contain the string "qxc" completely if this 3-gram could be found in the corpus.

So, which values for n should you choose? This is not an easy question. Generally speaking, small n values will lead to small segments and vice versa. But sometimes you may, for example, need n-grams for n = 2, 4 to get the best recognition performance. You will just have to test it.

Over the threshold

There is this last parameter you need, the threshold t. This is the cutoff value. If any position between two characters in the string has a calculated probability value that is larger than the threshold, it will be split at this point. Why do you need this? Usually, positions are split if the calculated value for the current position is a local maximum, which means that it is more likely that a word boundary lies on the current position then on the one directly before or after it. But sometimes, this just isn't enough. A simple example is the word "a", as in "not a simple example". To split this string correctly, the positions right before and right after "a" have to be split. But they are next to each other, so they can not both be local maxima, as this would mean they are each larger than the other. If you are a mathematician, you can now construct an algebra were this is possible, but we normal people will just have to set this to some value. As values can only vary between 0 and 1, 0.5 seem like a good natural compromise. But if you want to get the best value, you will have to test it, too.

Optimize

Guess what? I wrote a method to help you test which values for n and t are the best! You should probably do a "gem install maxixe" as soon as you can reach your terminal to get the new version.

I stole this idea from the paper, too. The idea is to split a small number of sentences (5 to 50) by hand and use these to figure out the parameters that work best for your use case. Now, the authors of the paper are proper scientists, so they used precision and recall to measure recognition performance. But I wrote this in 2 hours after work, so I will measure recognition performance by edit distance: diff the pre-segmented strings with the ones split by the algorithm, add up the edits needed to make them the same and use this as a score. A score of 0 means that there was no difference between your pre-segmented sentences and the ones split by the segmenter, so this is a perfect score. The higher the scores, the worse the word recognition.

Here is the example from maxixes readme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pre_segmented = [["MYDOGISINTHEHOUSE", "MY DOG IS IN THE HOUSE"],
                ["FOURNICEDOGS", "FOUR NICE DOGS"],
                ["MYCATLIKESMYDOG", "MY CAT LIKES MY DOG"]]
index = Maxixe::Trainer.generate_corpus_from_io([2,3,4,5], "ILIKEMYDOG
THISHOUSEISMYHOUSE
MYDOGISSONICE
WHOLIKESDOGSANYWAY
CATSANDDOGSUSUALLYFIGHT
INMYHOUSETHEREAREFOURDOGS
IWANTAHOUSEFORMYDOG")

optimal = Maxixe::Trainer.optimize(index, pre_segmented)

# optimal is now {:score => 0, :t => 0.5, :n => ["2","4"]}

As you can see, you will have to build an index first. This should contain any values for n you could possible want. The optimizer will try any combination of values for n and t and calculate the score. Then, it will give you the smallest result for score, n and t, in that order. The above example has the best recognition performance if you have n-grams with n = 2,4 and a threshold of 0.5. If we used a real corpus, we would now have a segmenter with nicely tuned parameters, ready to segment a lot of sentences.

BTW, there are more combinations of parameters that will give you a score of 0 for this example. If you want to see these, take a look at Maxixe::Trainer.check_recognition. 

What's next?

By now, you should have a working segmenter, finely tuned for your exact use case. So, you made a segmenter for biblical Greek? Or for Cicero's work? Why not make a gem out of it, so others can use it? I will do the same, so stay tuned for more...

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sat, 10 Sep 2011 11:58:00 -0700 Using Picky for search and profit http://rogerbraun.net/using-picky-for-search-and-profit http://rogerbraun.net/using-picky-for-search-and-profit

As you might remember from my post about my statistical segmenter, Maxixe, I have used the nearly pure Ruby search engine Picky in one of my current projects, wadoku.eu.

I had tried to use Solr before, but I just could not get it to work in any useful way. I made my own database-based search, but it seems I am an idiot. I just could not get the search times to be reasonably small, so I toyed with the idea of using the fulltext search included with SQLite. But then it would have been hard to switch DBs later, so I started looking for other solutions. Picky turned out to be exactly what I needed: Offline indexing, fast performance, pure Ruby (so I could actually take a look under the hood - more on that later) and easy to setup. If you want to get a quick taste of why I prefer Picky over Solr, just take a look at their respective homepages. Which one looks more accessible?

Picky and WaDokuJT

Wadoku.eu is an online interface for Ulrich Apel's WaDokuJT dictionary. As you might imagine, dictonary data is highly structured. WaDokuJT has about 200.000 entries, each a japanese word or phrase with several german translation equivalents, arranged in meaning groups. There is even more: Etymological information, notes on word usage, etc. WaDokuJT uses a simple markup language to structure these entries. I wrote parser for it in Citrus, but please don't look at it.

Anyway, there is a lot of structure and semantics in these entries, and just using a full text search would lose a lot of this. Picky brands itself as a "semantic text search engine" and is perfect for the job. It works like this:

  1. Give Picky some of your data to index (everything with an #each is indexable), define indexed categories and searches
  2. Run the Picky server
  3. That's it!

Picky then gives you a very simple JSON interface for your searches. You should try it, it's really easy and really fast. The beauty lies in how you define what you want to index and how it is searched. Take a look at the Picky Wiki for a taste.

I had a tab seperated file containing all entries, so I wrote a class that would respond to #each with every entry in the file, disguised as an object. Your object just has to respond to #id and whatever you name your categories. This made it easy to handle a problem I had: I wanted to make everything searchable not only by kanji and kana, but also by their transcription into latin characters (or, as we smart guys say, romaji). This is really easy. I have the reading in kana already in the file, so let's say my entry objects responds to #kana. I can then just write this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require "romkan"

class WadokuEntry
  def initialize(entry_as_string)
    @str = entry_as_string
  end

  def kana
    @str.split("\t")[1] # The reading is the second column in the file
  end

  def romaji
    kana.to_roma
  end
end

I can then add the category "romaji" to the index and it will work just as if it were part of the file. As you can see, transforming your data for use in indexing is very easy with Picky. Just write your Ruby and be happy.

I won't go into more detail here, but I want to talk about another thing that makes Picky great: It's author.

Fixing Picky

The data I want to search through contains a lot of what is (too often) considered to be special characters: umlautskanji, stuff like that. When I made Picky index my data, I noticed two things: First, that the partial index looked strange. Second, that the Picky server would not start at all. Bummer. I wrote a small example and filed a bug. Soon, Florian Hanke (Picky's father) answered with a detailed explanation of what he thought went wrong and explained how he debugged it. This was wonderful, as it enabled me to quickly find the source of the two bugs (one in Picky, one inyajl-ruby) and make some suggestions on fixing them. As it turns out, Picky had seemingly never been used for characters outside of the ASCII set before, as it usually does substitutions to get rid of diacritics before putting the tokens in the index. The indexing treated the symbols as ASCII, so it would cut off only one byte at a time when generating partials. This won't work for japanese characters, as they are 3 byte each. Also, yajl-ruby would not symbolize non-ASCII characters correctly, as it also treated everything as ASCII internally. There were some other UTF-8 related problems on the way (read the issue if you are interested), but everything was working about three days after I looked at Picky for the first time. That is less time then I spent on getting Sphinx or Solr to work.

Florian helped a lot with getting it to work, even though japanese was not his use case. I really got the feeling like this was the ideal way an open source project should work: Put your stuff out there, help other people fix bugs and contribute, make the software better for everyone. Florian clearly is passionate about Picky - and it's contagious!

Just use it, already!

So why should you use Picky?

  • It's fast to search and easy to setup
  • It's just Ruby, everywhere
  • If you have problems, you will get help
  • It has an octopus as mascot

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sat, 10 Sep 2011 11:57:00 -0700 Using Picky for search and profit http://rogerbraun.net/very-interegsting http://rogerbraun.net/very-interegsting

As this entry had a really stupid permalink by mistake, it can now be found at http://rogerbraun.net/using-picky-for-search-and-profit

 

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sat, 03 Sep 2011 07:58:00 -0700 Maxixe - A simple statistical segmenter for any language http://rogerbraun.net/maxixe-a-simple-statistical-segmenter-for-any http://rogerbraun.net/maxixe-a-simple-statistical-segmenter-for-any

[UPDATE: I wrote a follow up article on how to train your segmenter and tune the parameters!]

Recently, I wanted to use Picky for a dictionary app (and did, see WadokuJT). This worked great after fixiing some small problems (which I will talk about in a later post). 

Now, to use Picky, you simply have to tell it which data to index, and then it will perform some operations on it to make it searchable. One of these operations is the tokenization: The data is split in tokens which are then indexed seperately. This is rather straight-forward for western languages: Just split the string data on spaces and punctuation. So a string "I love my cat" would give you these tokens: ["I", "love", "my", "cat"].

Now, not all languages work this way. Many asian and other languages have scriptio continua, so they just write word after word after work, with no spaces (and sometimes, no punctuation). You can even get this with western languages, for example latin. So, if you want to tokenize text in these languages, you will have to find out where the word boundaries are.

In my case, Japanese, there are already several software packages that can split text into words, for example morphological analyzers like MeCab, ChaSen and Juman. These do more than we need, as they also analyze sentence structure and morphology of the words. Also, they are all rather large pieces of software that contain hand-written dictionaries and grammar rules to parse natural-language sentences. Further, they are often somewhat hard to install and run, the documentation often being available only in Japanese. And finally, if your language is not Japanese, but say, Chinese, or Linear B, you will have to take a completely different approach unless you want to rewrite the dictionary and grammar. I wanted something universal, written in Ruby, that I could just require from Picky.

So i went to Google Scholar and looked at the results for "japanese segmentation". I found a paper called "Mostly-unsupervised statistical segmentation of Japanese kanji sequences" by Ando and Lee. As it turns out, the algorithm described in this paper (called TANGO) is not really specific to japanese, and you can use it to segment pretty much any language.

So i wrote a gem, Maxixe, that implements this algorithm. Here is an example of what you can do with it.

Say you have a training file containing these unsegmented sentences: 

ILIKEMYDOG
THISHOUSEISMYHOUSE
MYDOGISSONICE
INMYHOUSETHEREAREFOURDOGS
IWANTAHOUSEFORMYDOG

You can use these to train the segmenter and split a sentence. It will detect word boundaries by itself.

hash = Maxixe::Trainer.generate_training_data([3,4], "dogs")
s = Maxixe::Segmenter.new(hash)
s.segment "MYDOGISINTHEHOUSE"
 => "MY DOG IS IN THE HOUSE"

Nice, isn't it? So how does this work? 

The algorithm is based on the idea that if you count any n-grams (substrings of length n) in a text, n-grams that are words or parts of word will appear more often than those which are just random strings created by two adjacent words. So what you do is use your training data to build a hash which contains the count of all n-grams in this corpus. Then, when you try to segment a string, you calculate if it is more probable that you are between two words or on a word, by comparing the count of n-grams to the left and right of the current position and the count of the n-grams that overlap with the current position. Then, after getting a numerical value for all positions, you split on all local maxima and if the value is above a certain threshold. I suggest that you read the paper yourself if you are interested. Once you grasp the underlying idea, it is quite easy to understand.

There are three things here that affect the segmentation performance: The corpus you use, the kinds of n-grams you use and your threshold. The corpus part is straightforward: Use a large amount of text that is similar to the one you want to split. The n-grams and threshold are more complicated. The above example works if you use a threshold of 0.5 and generate n-grams for n = 3,4, but you can easily find other values which will give the wrong results. Sadly, you will have to figure out which parameters are the best by segmenting a small number of sentences by hand and checking which parameters give the results most similar to your test data. I plan to add support for automating this to Maxixe, but right now, you will have to do it yourself.

And that's it! So now, if you ever need to tokenize some text that does not have spaces, give Maxixe a try. The code is small and pure Ruby, so be sure to take a look under the hood.

Atraente_chiquinha_pixinguinha.ogg Listen on Posterous

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun -
Tue, 14 Dec 2010 14:46:30 -0800 Hausaufgabe für praktische Lexikographie: Einfache Kanji-Statistik mit Ruby http://rogerbraun.net/hausaufgabe-fur-praktische-lexikographie-einf http://rogerbraun.net/hausaufgabe-fur-praktische-lexikographie-einf Jetzt zu der eigentlichen Aufgabe: Ladet euch folgende zwei Dateien herunter und benennt "kanji_statistics.rb.txt" in "kanji_statistics.rb" um (entfernt also das .txt). Lest die Kommentare in kanji_statistics.rb und versucht die Datei so zu ergänzen daß ein Aufruf von "ruby kanji_statistics.rb inugami.txt" euch die Häufigkeiten der Kanji zurück gibt. Aufgabe: Welche Kanji sind die drei häufigsten? Wie häufig kommen sie vor? BONUS: Sortiere die Kanji nach Häufigkeit! Schlage dazu die Methode "sort" nach (http://ruby-doc.org/core/classes/Hash.html#M002865). Wenn ihr Fragen habt kommentiert einfach oder schreibt eine Mail.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Tue, 14 Dec 2010 13:42:01 -0800 Hausaufgabe für praktische Lexikographie: Ruby-Installation und erste Schritte http://rogerbraun.net/hausaufgabe-fur-praktische-lexikographie-ruby http://rogerbraun.net/hausaufgabe-fur-praktische-lexikographie-ruby Für die Lösung dieser Aufgabe ist ein Mindestmaß an Ruby-Kenntnissen erforderlich. Wer noch keine hat sollte das Tutorial auf tryruby.org durchmachen, fast alle für dieses Problem nötigen Fähigkeiten werden in diesem vermittelt. Dann installiert euch Ruby in einer Version ab 1.9. Wer MacOS oder Linux benutzt schafft es hoffentlich selber (wenn nicht: melden!), aber für die Windowsnutzer gilt folgende Anleitung: Von http://rubyinstaller.org/ die Datei rubyinstaller-1.9.2-p0.exe herunterladen und ausführen. Bei der Installation unbedingt anwählen die Dateien zur PATH-Variable hinzuzufügen wie in den Screenshots. Danach auf Start -> Ausführen und "cmd" eingeben. Ein schwarzes Fenster mit einer Eingabeaufforderung erscheint und in diesem Fenster könnt ihr jetzt eine interaktive Ruby-Umgebung (wie auf tryruby.org) benutzen wenn ihr "irb" eingebt.
Media_httprbraunnetbl_cnrsf
Media_httprbraunnetbl_ceqnb
Media_httprbraunnetbl_iqccb
In irb könnt ihr jetzt eure Rubyfähigkeiten nach belieben austesten. Testet mal einfache Arithmetik wie "2 + 4" und Stringfunktionen wie "'Hallo'.reverse". Wenn ihr soweit gekommen seid könnt ihr bei der eigentlichen Aufgabe weitermachen.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Tue, 12 Oct 2010 12:17:57 -0700 Update: Studentenausweise http://rogerbraun.net/update-studentenausweise http://rogerbraun.net/update-studentenausweise So wie es aussieht liest jemand bei der Uni mein Blog (oder sie sind selbst drauf gekommen). Hier der Aufdruck auf einem ganz aktuellen Ausweis:
Media_httprbraunnetbl_drfea
Allerdings ist dies ein Ausweis der so verschickt wurde. Ob die Automaten auch diesen Aufdruck machen weiß ich leider nicht. Wenn ein Leser seinen Aufdruck gerade frisch aktualisiert hat würde ich mich über eine Rückmeldung freuen!

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Mon, 11 Oct 2010 14:05:15 -0700 Die Supereliteuni Tübingen (leider keine Geographiekenntnisse) http://rogerbraun.net/die-supereliteuni-tubingen-leider-keine-geogr http://rogerbraun.net/die-supereliteuni-tubingen-leider-keine-geogr Die Uni Tübingen hat ein neues Corporate Design! Corporate Design ist das, was man macht, wenn man so tun will als wäre man aktiv und würde alles zum besseren verändern. Zu diesem Anlass wurden an alle Mitarbeiter schöne kleine Pins verteilt. Die sollen uns hier nicht interessieren. Die Kartonkarte auf der die Pins drauf sind ist allerdings interessant. Hier das Bild:
Media_httprbraunnetbl_qdifk
Wer genau hinschaut kann ja mal gucken wo der rote Punkt, der wohl Tübingen darstellen soll, denn eigentlich wirklich drauf ist. Ich habe mal eine Deutschlandkarte drübergelegt und Tübingen schwarz markiert. Das Ergebnis:
Media_httprbraunnetbl_cfitb
Sieht also so aus als hätten wir das abgelegt Design irgendeiner Uni aus Berlin bekommen... Nunja. Dieser Fall zeigt wieder mal ganz gut wie eigener Anspruch und tatsächliche Fähigkeiten bei dieser Uni auseinander driften

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Mon, 20 Sep 2010 23:14:45 -0700 Psychic Debugging http://rogerbraun.net/psychic-debugging http://rogerbraun.net/psychic-debugging Der aktuelle Aufdruck auf den Studentenausweisen sieht ein wenig ungewöhnlich aus:
Media_httprbraunnetbl_earej
Falls man es nicht lesen kann: es ist dort das "WS10/011" angegeben. 011? Das ist ziemlich seltsam. Offensichtlich wird dort eine Null zuviel angefügt? Wie kann das passieren? (Nur damit es klar ist: Ich weiß nicht was wirklich los ist. Aber der folgende Weg erscheint mir wahrscheinlich. Der Code ist Ruby, sollte aber leicht zu verstehen sein.)

Pre-2k

Die letzten zwei Stellen des Jahres zu bekommen war bis zum Jahr 2000 ziemlich leicht: Es reichte einfach die Jahreszahl modulo 100 zu rechnen. Der Modulo-Operator gibt einem grob gesagt den Rest einer Division zurück und verhält sich in etwa wie die Zeitdarstellung auf einer Uhr: Sowohl bei 13 Uhr als auch bei ein Uhr wird eine eins angezeigt, da 13 / 12 = 1, Rest 1 ist (und 1 / 12 = 0, Rest 1). Wenn man also etwa 1999 modulo 100 rechnet kommt die 99 heraus – genau was wir haben wollen! Die erste Variante der "Jahr-zu-zwei-Stellen-Funktion" sah wahrscheinlich so aus:
def year_for_printing(year)
      return year % 100
    end

2000+

Ab dem Jahr 2000 funktionierte das aber nicht mehr, da 2000 % 100 einfach Null ergibt, also nur noch einstellig ist! Ich schätze der Fehler wurde wie folgt repariert:
def year_for_printing(year)
      if year 
Man hat also wohl einfach einen Zweig hinzugefügt, der bei den Jahren ab 2000 eine Null zum Ergebnis hinzufügt. Der Code gibt richtig aus: "WS 99/00". Das wird zehn Jahre gut gehen! Nämlich bis zum Jahre 2010...

 Hochspekulativer Zwischenschritt

Der folgende Schritt erscheint einfacher möglich wenn folgendes innerhalb dieser zehn Jahre passierte: Jemand hat sich die Funktion angeschaut und gesehen, dass sie den unnötigen Fall "vor dem Jahr 2000" abdeckt und diesen entfernt. Die neue Funktion sah also so aus:

    def year_for_printing(year)
      return "0" + (year % 100).to_s
    end

Die Argumentation ist zwar noch schlüssig wenn dieser Schritt fehlt, wirkt aber mit ihm wahrscheinlicher.

2010, 2011

Als das Jahr 2010 anbrach wurde die Ausgabe wieder falsch: Der Code gibt jetzt "010" zurück. Das steht aber offensichtlich nicht auf den Ausweisen. Ich schätze dass dem Programmierer der Auftrag gegeben wurde, den Fehler für das Jahr 2010 zu beheben. Er hat die Funktion wohl so abgeändert:

    def year_for_printing(year)
      if year == 2010
        return year % 100
      else
        return "0" + (year % 100).to_s
      end
    end

Dieser Code gibt richtig aus: "WS09/10". Es scheint also alles seine Richtigkeit zu haben! Aber beim Jahr 2011 gibt dieser Code auch genau das aus, was jetzt auf unseren Karten zu lesen ist: "WS10/011". Und das hat anscheinend noch niemand gemerkt. Oder nicht für wichtig genug gehalten.

Hier mein Bugfix für den Code. Ich gehe mal davon aus dass das System ein Embedded-System ohne Datumsfunktionen ist (denn diese sollte man benutzen, wenn sie vorhanden sind!).

    def year_for_printing(year)
      res = year % 100
      if res 

Dieser Code gibt für jedes Jahr den richtigen Wert aus. Wenn ihr das Spiel gerne weitertreiben wollt, dann ersetzt einfach "year == 2010" durch "year >= 2010" und ihr habt wieder Code der nur die nächsten 10 Jahre richtig ist.

Habt ihr eine andere Erklärungsmöglichkeit?

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Wed, 18 Aug 2010 15:23:39 -0700 Podcasts hosten auf Amazon S3 http://rogerbraun.net/podcasts-hosten-auf-amazon-s3 http://rogerbraun.net/podcasts-hosten-auf-amazon-s3 Wie manche Wissen mache ich mit ein paar anderen Leuten einen skeptischen Podcast auf http://humoralpathologie.de. Bisher habe ich die Dateien auf meinem VPS-Server gehostet. Der ist aber einerseits etwas lahm, andererseits will ich nicht plötzlich mit einer Riesenrechnung für den Traffic konfrontiert werden. Die Lösung ist recht einfach: Amazon S3, ein Storage-System in der Cloud mit sehr günstigen Preisen. Der Speicher kostet derzeit 0.15 $ pro Gigabyte und pro Monat, was bei der momentanen Podcastgröße von etwa 50 MB pro Datei immerhin 20 Podcasts bedeutet. Da wir etwa alle 2 Wochen einen neuen Podcast aufnehmen müssen wir also Ende des Jahres mit monatlichen Speichergebühren von etwa 30 Cent rechnen. Datenübertragung bis zu einem Gigabyte im Monat sind sogar kostenlos und fangen danach ebenfalls bei 15 US-Cent pro GB an. Momentan haben wir etwa 400 Downloads pro Monat, was Kosten unter 3 Euro verursacht. Die Konfiguration ist sehr einfach, daher werde ich hier keine Anleitung liefern: Man lädt einfach die Dateien dort hoch und bekommt einen Downloadlink. Wie hostet ihr eure Podcasts?

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Wed, 18 Aug 2010 02:26:15 -0700 Wider die großen Worte http://rogerbraun.net/wider-die-grossen-worte http://rogerbraun.net/wider-die-grossen-worte Auf Zeit.de findet sich (jedenfalls soweit dies der OCR-Software möglich war) ein Artikel von Karl Popper, in dem er seine Kollegen kritisiert, sich zu unklar auszudrücken. Dies zum Teil mit voller Absicht, um den eigenen Gedanken Gewicht zu verleihen.
Was ich 'die Sünde gegen den heiligen Geist genannt habe — die Anmaßung des dreiviertel Gebildeten —, das ist das Phrasendreschen, das Vorgeben einer Weisheit, die wir nicht besitzen. Das Kochrezept ist: Tautologien und Trivialitäten gewürzt mit paradoxem Unsinn. Ein anderes Kochrezept ist: Schreibe schwer verständlichen Schwulst und füge von Zeit zu Zeit Trivialitäten hinzu. Das schmeckt dem Leser, der geschmeichelt ist, in einem so „tiefen" Buch Gedanken zu finden, die er schon selbst einmal gedacht hat. (Wie heute jeder sehen kann — des Kaisers neue Kleider machen Mode!)
Vor kurzem erreichte mich eine Email mit einer Einladung zu einem Gender-Workshop in Frankfurt. Kostprobe:
Wird die Normativität der dualistisch angelegten Geschlechter-Matrix aufgehoben? Inwiefern ist eine Diversifizierung der Männlichkeits- und Weiblichkeitsbilder festzustellen? Wie wirkt sich die Pluralisierung der Lebensstile aus? Was bedeutet die zunehmende Differenzierung innerhalb der Gruppen der Frauen und Männer und welche Bedeutung haben dabei Machtrepräsentationen im Spannungsfeld der Intersektionalität?
Die Geschlechtermatrix kann übrigens manchmal auch binär oder bipolar sein! Hier übergebe ich wieder an Popper, der wiederum Goethe zitiert:
Gewöhnlich glaubt der Mensch, wenn er nur Worte hört, es müsse sich dabei doch auch was denken lassen.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Thu, 11 Feb 2010 20:05:07 -0800 Hofpfisterei und Typografie http://rogerbraun.net/hofpfisterei-und-typografie http://rogerbraun.net/hofpfisterei-und-typografie Die Hofpfisterei (eine Bäckerei für Leute die auch zum Coiffeur gehen) hat eine Filiale in Tübingen am Marktplatz. Beim letzten vorbeispazieren fiel mir auf, dass die Schrift des Namenszugs beim "fi" weder Kerning noch eine Ligatur benutzt. Das sieht komisch aus, gerade weil das beim "fp" kurz davor noch eingerückt wird, beim "fi" aber ein riesiges Loch ist. Ansichtsexemplar:
Media_httprbraunnetbl_jytdv
Ich hab ein bisschen damit rumgespielt und das "fi" ligaturisiert. Sieht immer noch irgendwie doof aus, was aber eher an meiner Fontdesignschwäche als an der Idee liegen sollte. Ergebnis:

Media_httprbraunnetbl_feddh

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sat, 06 Feb 2010 17:46:15 -0800 Ein sehr einfaches Translation-Memory-System mit Sinatra http://rogerbraun.net/ein-sehr-einfaches-translation-memory-system http://rogerbraun.net/ein-sehr-einfaches-translation-memory-system Wie manchem bekannt arbeite ich zur Zeit mit einigen Anderen an einer Übersetzung von Sanshôdayû, aber nicht dem Film sondern dem klassisch japanischen Original aus dem 17. Jhd. Wir benutzen zur Übersetzung eine selbst geschriebene Webanwendung mit der wir jeweils zu kurzen Textabschnitten Kommentare, Anmerkung, Fragen und natürlich die fertigen Übersetzungen schreiben und darüber diskutieren können. Ein Problem dass sich öfter stellt ist dass man eine Formulieren oder ein Wort schon mal übersetzt hat, aber sich nicht mehr erinnern kann wie genau. Wenn man nun jedesmal anders übersetzt können aus im Original gleichen Formulierungen in der Übersetzung völlig verschiedene Sätze werden. Die Struktur des Textes wird dadurch verfälscht. Zum Glück hat man natürlich längst Lösungen für so etwas erfunden (auch wenn die niemand benutzt)!  Übersetzungsspeicher halten Quell- und Zieltexte in einer Datenbank vor und stellen Möglichkeiten zur Verfügung zu einem gegebenen Quellsatz eine mögliche Übersetzung zurückzuliefern. In unserem Beispiel werden die in der Datenbanken vorhandenen Sätze auf ihre Ähnlichkeit geprüft und danach sortiert. Ich habe ein (sehr) einfaches TM-System mit Sinatra und Monk geschrieben. Der Code ist auf Github. Die Datenbank stellt jeden Eintrag als ein Tupel dar, dass aus Quell- Und Zieltext besteht. Sucht man einen Satz wird dieser mit jedem Quellsatz in der Datenbank verglichen und seine Levenshtein-Distanz berechnet. Da es hierfür schon Bibliotheken gibt ist der eigentliche Suchalgorithmus sehr klein.
def get_best_matches(phrase)
      $KCODE = "u"
      Translation.all.map{|item| [::Text::Levenshtein.distance(phrase,item.source),item.source,item.target]}.sort_by{|item| item[0]}
end
Mal schauen ob's bei der Übersetzung hilft.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Sat, 23 Jan 2010 12:06:23 -0800 Einfacher rekursiver Sortieralgorithmus in Ruby http://rogerbraun.net/einfacher-rekursiver-sortieralgorithmus-in-ru http://rogerbraun.net/einfacher-rekursiver-sortieralgorithmus-in-ru
def insert_into_sorted_helper(sorted, element, rest)

  if rest.size == 0 then
    sorted << element   
  elsif rest[0] >= element then
    (sorted << element).concat(rest)
  else
    insert_into_sorted_helper(sorted << rest[0], element, rest[1..-1])
  end 

end

# Insert an element into a already sorted list. The resulting list stays sorted.
def insert_into_sorted(sorted, element)
  insert_into_sorted_helper([],element,sorted)
end

def recursive_sort_helper(sorted, unsorted)

  if unsorted.size == 0 then
    sorted
  else
    first = unsorted[0]
    rest = unsorted[1..-1]
    recursive_sort_helper(insert_into_sorted(sorted, first), rest)
  end
end

# Sorts a list
def recursive_sort(unsorted)
  recursive_sort_helper([],unsorted)
end

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Tue, 08 Dec 2009 18:52:37 -0800 Japanologie, Bachelor, Unistreiks http://rogerbraun.net/japanologie-bachelor-unistreiks http://rogerbraun.net/japanologie-bachelor-unistreiks
Der folgende Beitrag ist eine Reaktion auf einen Gastbeitrag bei Astrodicticum simplex, daher sollte man sich diesen vorher durchgelesen haben.
Ich studiere ebenfalls Japanologie, allerdings in Tübingen. Ich selbst studiere noch die Magistervariante, kenne aber die Nöte meiner Bachelor/Masterkommilitonen durchaus. Hier sieht die Situation zum Teil ähnlich aus. Die Anzahl der Neuanfänger wurde auf 24 Hauptfächler und ebenso viele Nebenfächler reduziert, Geld für eine weitere Lektorenstelle (oder gar eine Professur) ist natürlich ebenfalls nicht vorhanden. Soweit kann ich die Probleme nur bestätigen. Einen NC halte ich allerdings für eine absolut katastrophale Lösung des Problems. Genau so gut könnte man Würfeln oder Lose ziehen. Jemandem der sein Mathe-Abitur mit 1,0 bestanden hat eine bessere Eignung für das Studium der Japanologie zu bescheinigen als dem, der leider nur eine 3 geschafft hat, ist absurd. So weit ich weiß gibt keine Statistiken die einen Zusammenhang zwischen Abiturnote (die sich ja aus wer-weiß-was zusammensetzen kann) und den späteren Noten im Japanologiestudium nachweisen. Das NC-System wird nur deshalb zur Begrenzung eingesetzt weil es irgendwie gerecht und objektiv aussieht. Ich denke ein Lossystem wäre gerechter. Die Kritik an den schwammigen Aussagen kann ich ebenfalls nicht nachvollziehen. "Wir wollen doch was lernen, und das möglichst schnell, damit wir schnell arbeiten können" ist die vom Bachelorsystem aufgezwungene Vorstellung des Sinns der Universität. Hier so zu tun als sei die (faktisch vorhandene) Verschulung des Studiums nur eine Problem für Faule, die eigentlich gar kein Interesse haben, finde ich schon beinahe beleidigend. Sicherlich gibt es viele die einfach nur möglichst schnell studieren wollen um möglichst bald in einen Job zu kommen. Die konnten das aber auch schon im alten Magister machen. Das Bachelorsystem beraubt einen der Möglichkeit, sein Lerntempo (und auch den Lehrstoff) selbst zu bestimmen. Gerade in der Japanologie wird das Studium dadurch zu einem geadelten Japanischkurs, was es eben eigentlich nicht ist. In den ersten 2 Jahren (ca.) lernt man hauptsächlich die japanische Sprache und grundlegende Kenntnisse über Wirtschaft, Geschichte und Gesellschaft. Der Bachelorstudent hat jetzt noch ein Jahr um sich echte Inhalte anzueignen, was absolut illusorisch ist. Der Bachelorstudiengang ohne anschließenden Master ist in der Japanologie vollkommen unsinnig, da einem schlicht die Zeit fehlt sich über die Kenntnisse des Japanischen hinaus etwas anzueignen (und vielleicht auch mal einen Kurs in der Indologie, in der Informatik, in den Religionswissenschaften zu besuchen). Aber im Sinne der Gleichmacherei bei der Einführung der neuen Studiengänge wurde so etwas natürlich nicht bedacht. Wohlgemerkt: Der Bachelor ist ein sehr guter Japanischkurs! Aber eben auch nicht viel mehr. Das Problem an der Anwesenheitspflich ist, dass (wieder einmal) sinnvolle allgemeine Regeln (bei mangelnder Anwesenheit kein bestehen) durch unsinnig spezifische, bürokratische und unpersönliche Vorschriften ersetzt werden (3 mal gefehlt -> durchgefallen). Da die Bachelorstudenten sich auch nicht während eines Kursen entscheiden können, ihn doch erst später zu machen, sondern vor Beginn des Kurses sich fest für diesen Anmelden müssen, bedeutet es für einige die Exmatrikulation wenn sie einmal zu oft unentschuldigt fehlen. Regelungen zur Anwesenheit sollten immer individuell sein. Muss diejenige, die schon 2 Jahre in Japan gelebt hat und jede Sprachklausur mit Bestnoten abschließt wirklich in jeder Stunde anwesend sein? Das zusätzlich auch noch JEDE Note in das Bachelorzeugnis mit eingeht verstärkt den Stress noch: Plötzlich muss man in jedem Kurs gut sein. In einem thematisch so breiten Fach wie der Japanologie ist das aber ebenfalls unsinnig: Warum sollte ich besser klassisches Japanisch aus dem 12. Jahrhundert übersetzen können wenn ich im Toyota-Kurs gut war (andersrum gilt natürlich das selbe)? Ein vorläufiges Schlusswort: Die Umstellung auf das BA/MA-System ist eine Einschränkung der Freiheit der Studenten wie der Dozenten, die Universität so zu gestalten wie sie es für sinnvoll halten. Inhalte werden bürokratischen (angeblichen) Zwängen untergeordnet, Studenten werden zum durchhecheln des vorgegebenen Stoffes gezwungen. Dich selbst mit den eigenen Interessen zu beschäftigen wird nahezu unmöglich. Wissenschaftlich kann dies der deutschen Japanologie auf Dauer nur Schaden. Das sich das einzige verbliebene deutsche Journal (die "Japanstudien") nun in "Contemporary Japan" umbenannt hat und auf Englisch als Hauptsprache umstellt ist da ja schon fast nur konsequent. Ob die Streiks hier etwas bringen weiß ich nicht. Das jedoch anscheinend endlich auch bei den Studenten das Bewusstsein wächst, dass hier etwas nicht in Ordnung ist, kann ich nur begrüßen.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun
Wed, 02 Dec 2009 23:55:33 -0800 JLPT-Daten in mundgerechten Stücken für Filemaker http://rogerbraun.net/jlpt-daten-in-mundgerechten-stucken-fur-filem http://rogerbraun.net/jlpt-daten-in-mundgerechten-stucken-fur-filem Heute mal ein wenig Ruby. Wir wollen die JLPT-Dateien des EDICT-Projekts so umwandeln, das man die Datei leicht in eine Filemaker-Datenbank importieren kann. Man könnte die Umwandlung auch in Filemaker selbst vornehmen, allerdings ist das deutlich komplizierter. Das Standardformat der JLPT-Dateien ist
Schreibung [Lesung] /Bedeutung1/Bedeutung2.../Bedeutungen/
Es kommt also erst die Schreibung, dann die Lesung in eckigen Klammern und schließlich die englischen Übersetzungen in Slashes. Um eine Datei zum Lesen zu öffnen kann man in Ruby folgendes schreiben:
input = File.open("jlpt-voc-4-extra.utf")
Die Datei kann man jetzt über input ansprechen. Eine Ausgabedatei brauchen wir auch noch, also machen wir
output = File.open("jlpt-converted.tab","w:UTF-16LE")
Wir wollen eine tabseparierte Ausgabedatei erstellen, weil diese leicht von Filemaker importiert werden können. Als Ausgabekodierung setzen wir UTF-16. Nun müssen wir die Datei Zeilenweise durchgehen und in die Einzelteile auftrennen. Die Datei-Klasse von Ruby kann hierfür each verwenden. Ein Beispiel:
input.each do |line|
  puts line
end
Dieser Code würde jede Zeile einmal ausgeben. Puts gibt das aus was ihm übergeben wird und beginnt eine neue Zeile. Die Schreibung ist alles was sich links vom ersten Leerzeichen befindet. Wenn wir die Zeile an den Leerzeichen in kleine Teile zerhacken könnten wäre es also das erste Stückchen. Genau das macht der split-Befehl. Unser neues Beispiel:
input.each do |line|
  schreibung = line.split(" ")[0]
  puts schreibung
endNun gibt unser Code die Schreibung aus! line.split(" ") gibt ein Array zurück, also eine Liste von Stückchen. Diese Stückchen lassen sich über [Nummer] ansprechen. Da bei 0 angefangen wird zu zählen ist das erste Stückchen also [0].

Die Lesung zu filtern ist schon etwas schwerer. Die Lösung:input.each do |line|
  schreibung = line.split(" ")[0]
  lesung = line[/\[(.*)\]/,1] || schreibung
  puts schreibung + " " + lesung
end
Das sieht kompliziert aus, ist aber eigentlich auch einfach. Das zwischen den eckigen Klammern bei line ist ein regulärer Ausdruck. Reguläre Ausdrücke sind speziell dafür gemacht um Textstücke herauszufiltern. In Ruby sind reguläre Ausdrücke immer von Slashes (/Ausdruck/) umgeben, der eigentliche Ausdruck in unserem Fall ist also \[(.*)\]. In regulären Ausdrücken passen bestimmt Zeichen auf bestimmte andere Zeichen. Wenn ein Zeichen passt wird es herausgefiltert. Alphanumerische Zeichen passen auf sich selbst. Beispiele:"Hallo, wie geht's?"[/Hallo/] => "Hallo"
"Hallo, wie geht's?"[/llo/] => "llo"
"Hallo, wie geht's?"[/Bundespräsident Horst Köhler/] => nilGibt es keine passenden Zeichen gibt der Ausdruck nil zurück, was in Ruby "kein Ergebnis" bedeutet. Uns interessieren aber nun beliebige Zeichen, die sich zwischen zwei eckigen Klammern befinden. Wir wollen also etwas wie [irgendwas]. Leider habe eckige Klammern schon eine besondere Bedeutung in regulären Ausdrücken, d.h. sie passen nicht auf sich selbst. Eckige Klammern passen auf \[ und \]. Unser neuer Ausdruck ist also \[irgendwas\]. Jetzt müssen wir nur noch irgendwas als regulären Ausdruck schreiben. Wir wollen eine beliebige Anzahl von beliebigen Zeichen filtern. Ein Punkt (also".") passt auf genau ein beliebiges Zeichen. "[a]"[/\[.\]/] würde also "[a]" zurückgeben. Auf "[aaaa]" würde es aber schon nicht mehr passen! Zum Glück gibt es auch den Ausdruck *, der "beliebig viele vom vorherigen Zeichen" bedeutet. .* bedeutet also "beliebig viele beliebige Zeichen. Genau was wir brauchen! [/\[.*\]/] passt also auch auf [わたし] und gibt in diesem Fall das selbe zurück."赤い [あかい] /(adj) red/(P)/"[/\[.*\]/] würde "[あかい]" zurückgeben. Um die Klammern los zu werden machen wir eine Gruppe aus den "beliebigen Zeichen", indem wir normale Klammern um sie herum machen. Jetzt gibt uns der Ausdruck mehrere Gruppen zurück. Mit ",1" sagen wir ihm, das wir die zweite Gruppe haben wollen. Die erste Gruppe ist alles was passt (also "[あかい]") und hat die Nummer 0. Unsere zweite hat die Nummer 1, also ist unser endgültiger Ausdruck [/\[(.*)\]/] und wird so verwendet:lesung = line[/\[(.*)\]/,1]Manche Einträge haben aber in der JLPT-Datei gar keine Lesung! Katakana-Wörter stehen nur so da, unser Ausdrück würde also nil zurückgeben, da keine passenden Stücke gefunden werden. Dafür ist nun  || schreibung da. Es bedeutet "Wenn das vorherige nil war, dann nimm das folgende". Wenn also keine Lesung in der JLPT-Datei steht wird einfach die Schreibung als Lesung genommen. Wenn man sich nun noch mal den Code oben ansieht wird man merken dass er gar nicht so schwer ist wie zunächst gedacht. Die Bedeutung bekommt man genau so heraus, darum hier das fertige Programm:input = File.open("jlpt-voc-4-extra.utf")
output = File.open("jlpt-converted.tab","w:UTF-16LE")
input.each do |line|
  schreibung = line.split(" ")[0]  
  lesung = line[/\[(.*)\]/,1] || schreibung
  bedeutung = line[/\/(.*)\//,1]
  output.puts "#{schreibung}\t#{lesung}\t#{bedeutung}" unless line[0] == "#" || line.strip == ""
end
output.close
input.close
puts wird nun auf output angewendet und schreibt darum alles in eine Datei. \t ist das Zeichen für Tabulator und das ganze wird nur in die Datei geschrieben wenn das erste Zeichen nicht "#" ist oder die ganze Zeile leer ist.

Permalink | Leave a comment  »

]]>
http://files.posterous.com/user_profile_pics/1452812/photo.jpg http://posterous.com/users/hesadF80sMqg2 Roger Braun rogerbraun Roger Braun