<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[HarveyNick.com]]></title><description><![CDATA[HarveyNick.com]]></description><link>https://harveynick.com/</link><image><url>https://harveynick.com/favicon.png</url><title>HarveyNick.com</title><link>https://harveynick.com/</link></image><generator>Ghost 1.25</generator><lastBuildDate>Mon, 01 Oct 2018 13:37:59 GMT</lastBuildDate><atom:link href="https://harveynick.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Suddenly, a Wild “Now” Page Appears]]></title><description><![CDATA[A short explanation of the new “Now” page which has appeared in the site menu.]]></description><link>https://harveynick.com/2018/08/07/suddenly-a-wild-now-page-appears/</link><guid isPermaLink="false">5b69fa091f016d00bf3138f1</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Tue, 07 Aug 2018 20:00:29 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>There’s a <a href="https://harveynick.com/now/">new page</a> in the menu of this site. First of all I’ll make an admission: the idea is shamelessly stolen from <a href="http://www.jacobyyoung.com/now">Jacoby Young</a>. Hopefully it’s meaning is fairly self explanatory. It’s mostly there for my own self reflection, which I’ve decided to do in public, apparently.</p>
<p>My working plan is that I’ll update this page about once a month. I suspect that the favourite podcasts list to stay mostly unchanged over time, but I can see almost everything else changing on a monthly basis.</p>
<p>Something else: right now it says I’m reading three books. That’s mostly because I found Joon Ha Lee’s <a href="http://www.amazon.co.uk/dp/1781084483/?tag=harveyncom-21">Ninefox Gambit</a> hard enough going that I needed to take a break after the first few chapters. That book is <em>work</em>.</p>
</div>]]></content:encoded></item><item><title><![CDATA[A Swing and a Miss: Trying to Reduce English Uncertainty in IMDB Review Classification]]></title><description><![CDATA[My mostly unsuccessful attempt to use the part of speech tagging to reduce ambiguity and improve performance in IMDB review classification with RNNs.]]></description><link>https://harveynick.com/2018/07/22/a-swing-and-a-miss-trying-to-reduce-english-uncertainty-in-imdb-review-classification/</link><guid isPermaLink="false">5b54c5b6b0f91000bfc9f205</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 22 Jul 2018 18:15:14 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>When I was following the natural language processing / recurrent neural networks<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> section of <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.278&amp;type=3&amp;subid=0">Andrew Ng’s Deep Learning Specialisation</a>, there was a detail which bothered me. Now, having reached the same subject area in <a href="http://course.fast.ai/lessons/lesson4.html">fast.ai’s own deep learning course</a>, the same detail is bothering me again. The structure of the fast.ai course is much more open ended, so I could indulge myself and try to figure it out.</p>
<p>In case the title wasn’t a big enough give away: I was largely unsuccessful. But I’m not convinced I’m wrong, and the specifics of my failure suggest that it might be worth pulling on this thread a little more. Also: I think it’s important to publish negative results as well as positive ones.</p>
<p>Before I get to the detail in question: some background.</p>
<h2 id="howneuralnetworksunderstandnaturallanguage">How Neural Networks Understand Natural Language</h2>
<p>In short: they don’t. They understand numbers; or lists of numbers; or lists of lists of numbers; and so on.</p>
<p>The simplest way of converting words into a form which can be understood by a neural network is called a “one hot encoding”. Let’s say all of the text you care about uses only the 1000 most common english words, as per <a href="https://xkcd.com/thing-explainer/">Randall Munroe’s Thing Explainer</a>. So then each word is represented by a list of 1000 numbers, exactly one of which is <code>1</code>, whilst the rest are <code>0</code>. If “a” is the first word, then it will be represented a <code>1</code> followed by 999 <code>0</code>s.</p>
<p>This has the benefit of being simple. However, it has the disadvantage that it can’t generalise. A system trained against one hot encodings can only ever understand the words it was originally trained against. Furthermore, it has to explicitly learn their relationship to each other.</p>
<p>A more advanced alternative is to use word vectors. Here each word is represented by a smaller list of numbers of any value, though probably between <code>0</code> and <code>1</code>. You can think of these as being coordinates in space (though it might be a space with over a hundred dimensions). Words with similar meanings are close together in this space. So the vectors which represent the words <em>nice</em> and <em>pleasant</em> are likely to be similar. If the word vectors are well tuned, simple maths should also be possible. For example <code>v['king'] - v['male'] + v['female'] == v['queen']</code> should hold true.</p>
<p>I’m not going to go too deeply into this, or how the vectors themselves are created. If you want to find out more, check out <a href="https://en.wikipedia.org/wiki/Word2vec">word2vec</a>, which is probably the most well known Implementation.</p>
<p>The crux of this is that language model trained against word vectors rather than one hot encodings has the potential to generalise. The word <em>nice</em> might not have appeared in the corpus it was training against, but if its synonym <em>pleasant</em> was, then (given the embeddings) it can probably work with that. For a really well trained model, it could potentially even cope with never having seen <em>queen</em> if <em>king</em>, <em>male</em> and <em>female</em> were all in the training corpus.</p>
<h2 id="theambiguityoftheenglishlanguage">The Ambiguity of the English Language</h2>
<p>Here’s an example from Groucho Marks:</p>
<blockquote>
<p>One morning I shot an elephant in my pyjamas. How he got into my pyjamas I'll never know.</p>
</blockquote>
<p>I’m not even going to go there. That’s at least a level of abstraction higher than the problem I have in mind. Namely: the syntactic and semantic ambiguity of individual words. For example: <em>Still</em> is a very ambiguous word, having <a href="https://www.google.com/search?safe=off&amp;client=safari&amp;rls=en&amp;ei=io46W-2iLYO3kwWJ5K2wDw&amp;q=define%3Astill&amp;oq=define%3Astill&amp;gs_l=psy-ab.3..0i71k1l8.0.0.0.255811.0.0.0.0.0.0.0.0..0.0....0...1c..64.psy-ab..0.0.0....0.9ssfp72Nmaw" title="define:still">8 possible interpretations</a>. These include:</p>
<ul>
<li>As an <a href="https://www.google.com/search?safe=off&amp;client=safari&amp;rls=en&amp;ei=io46W-2iLYO3kwWJ5K2wDw&amp;q=define%3Astill&amp;oq=define%3Astill&amp;gs_l=psy-ab.3..0i71k1l8.0.0.0.255811.0.0.0.0.0.0.0.0..0.0....0...1c..64.psy-ab..0.0.0....0.9ssfp72Nmaw" title="define:still">adjective</a>, it is a synonym for “unmoving”;</li>
<li>As a <a href="https://www.google.com/search?safe=off&amp;client=safari&amp;rls=en&amp;ei=io46W-2iLYO3kwWJ5K2wDw&amp;q=define%3Astill&amp;oq=define%3Astill&amp;gs_l=psy-ab.3..0i71k1l8.0.0.0.255811.0.0.0.0.0.0.0.0..0.0....0...1c..64.psy-ab..0.0.0....0.9ssfp72Nmaw" title="define:still">verb</a>, it means “cause to be unmoving”, a rough synonym of “quieten”;</li>
<li>As a <a href="https://www.google.com/search?safe=off&amp;client=safari&amp;rls=en&amp;ei=io46W-2iLYO3kwWJ5K2wDw&amp;q=define%3Astill&amp;oq=define%3Astill&amp;gs_l=psy-ab.3..0i71k1l8.0.0.0.255811.0.0.0.0.0.0.0.0..0.0....0...1c..64.psy-ab..0.0.0....0.9ssfp72Nmaw" title="define:still">noun</a> (here we go) it may refer to:
<ul>
<li>A still image, especially one taken from a movie, so a rough synonym of “photograph”;</li>
<li>Deep silence;</li>
<li>The equipment used to make alcohol, as in “distillery”(!).</li>
</ul>
</li>
</ul>
<p>It might not even be the most troublesome example. Consider “seed”, which as a well as being a noun has <a href="https://www.google.com/search?safe=off&amp;client=safari&amp;rls=en&amp;ei=qpA6W7_ILc_isAf7vIn4BQ&amp;q=dictionary&amp;oq=dictionary&amp;gs_l=psy-ab.3..0i67k1l2j0j0i20i263k1j0i131k1l2j0l4.2395.4132.0.4369.10.9.0.1.1.0.167.784.0j6.6.0....0...1c.1.64.psy-ab..3.7.801...0i131i67k1.0.7MYTjOu7_VM#dobs=seed" title="dictionary">several possible interpretations as a verb</a>. The first means, essentially, “to add seeds”. One of the others? “To <em>remove</em> seeds”<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<h2 id="putthattogetherandwhathaveyougot">Put That Together and What Have You Got?</h2>
<p>Hopefully you see the problem. For the word vectors to work the way they’re usually constructed, the vector which represents <em>still</em> somehow has to encode it’s similarity with the words <em>unmoving</em>, <em>photograph</em> and <em>distillery</em>. <em>seed</em>’s vector must encode two meanings which are essentially opposites.</p>
<p>I’m not saying that’s actually impossible, but that it makes it very likely that some important nuance will be lost. It seemed pretty odd to me that Andrew Ng didn’t mention this at all. Jeremy Howard has also not mentioned it thus far in the fast.ai course (disclaimer: I’m not finished with this course yet, so he still might).</p>
<p>I can think of three possible reasons for this:</p>
<ol>
<li>It’s a new idea, or at least not well developed;</li>
<li>It’s been tried, and it doesn’t make any difference;</li>
<li>It’s a more advanced topic.</li>
</ol>
<p>I made a conscious decision not to look into either latter possibility. I will later, but first I wanted to dive in and see what I found. Sometimes it’s worth just trying something with minimal background reading. If you don’t know how possible something is thought to be, you’re less likely to be limited by that knowledge. Of course you might just be repeating the mistakes of others, but that too can be a learning experience.</p>
<h2 id="myattemptatasolution">My Attempt at a Solution</h2>
<p>My plan was pretty simple: don’t look up the word vector using just the word, use the <a href="https://en.m.wikipedia.org/wiki/Part-of-speech_tagging">part-of-speech tag</a> as well. To put it another way: use different vectors for <em>still</em> when used as a verb, and <em>still</em> when used as an adjective.</p>
<p>The fast.ai lesson 4 jupyter notebook begins by training a language modelling RNN from scratch against the IMDB reviews data-set, building new word vectors along the way. This RNN is then retrained as a classifier which identifies the sentiment of a particular review. This seemed like an ideal test case for my idea, on the surface at least.</p>
<p>The line of code responsible for building the vocabulary for the data-set looks like this:</p>
<pre><code class="language-python">TEXT = data.Field(lower=True, tokenize='spacey')
</code></pre>
<p>The <code>tokenize</code> parameter is ultimately resolved <a href="https://github.com/pytorch/text/blob/master/torchtext/data/utils.py">here</a>, with code which looks like this:</p>
<pre><code class="language-python">import spacy
spacy_en = spacy.load('en')
return lambda s: [tok.text for tok in spacy_en.tokenizer(s)]
</code></pre>
<p><code>tokenize</code> can be either a callable entity, or a string which represents a known tokeniser, as above<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. Spacey already performs part of speech tagging as part of the tokenisation process, so I can use the following to get the effect that I want:</p>
<pre><code class="language-python">import spacy
spacy_tok = spacy.load('en')
tok_pos = lambda s: [tok.text + &quot;-&quot; + tok.pos_.lower() for tok in spacy_tok(s)]
TEXT = data.Field(lower=True, tokenize=tok_pos)
</code></pre>
<p>Now, when tokenised, the phrase “Still the film still” becomes <code>['still-verb', 'the-det', 'film-noun', 'still-adv']</code> instead of <code>['still', 'the', 'film', 'still']</code>. Alternatively, for a more fine-grained output, I can use this in the lambda:</p>
<pre><code class="language-python">[tok.text + &quot;-&quot; + tok.tag_.lower() for tok in spacy_tok(s)]
</code></pre>
<p>This yields more information about the part of speech. So whereas <code>pos_</code> might yield “verb”, <code>tag_</code> will provide the type of verb e.g. “transitive verb”. This might might reduce the ambiguity even further, or it might just make it harder for the system to build good vectors.</p>
<h2 id="theeffectonbuildingthemodel">The Effect on Building the Model</h2>
<p>Both of my modified tokenisation strategies take a lot longer than the default. Rather than taking fifteen to twenty minutes to build the <code>TEXT</code> field, they take two to three hours. The resulting change in vocabulary size is significant, though.</p>
<div class="wide">
  <canvas id="vocab"></canvas>
</div>
<script>
  window.hvynck_charts["vocab"] = {
    type: 'bar',
    data: {
      labels: [
        'Original',
        'Simple Tags',
        'Complex Tags',
      ],
      datasets: [{
        label: "Vocabulary Size",
        backgroundColor: [
          window.hvynck_clrs.RED,
		  window.hvynck_clrs.BLUE,
		  window.hvynck_clrs.YELLOW,
        ],
        data: [ 37392, 44599, 46108 ],
      },],
    },
    options: {
      responsive: true,
      legend: { display: false, },
      tooltips: {mode: 'nearest', intersect: false, },
      hover: { mode: 'nearest', intersect: false, },
      title: {
        display: false,
        text: 'Vocabulary Size',
      },
      scales: {
        yAxes: [{
          scaleLabel: { 
            display: true,
            labelString: "Vocabulary Size",
            padding: 0,
          }, 
		  ticks: {min: 0},}],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Model</th><th>Vocabulary Size</th></tr>
<tr><td>Original</td><td>37392</td></tr>
<tr><td>Simple Tags</td><td>44599</td></tr>
<tr><td>Complex Tags</td><td>46108</td></tr>
</table>
</noscript>
<p>Likely as a result of this increased vocabulary, the per epoch training time for the language modelling phase increases from around seven and half minutes per epoch to around nine minutes per epoch.</p>
<p>As a first step, I followed identical training schedules for all three options. There are thirty-five epochs in fast.ai’s schedule so the training time increases from 260 minutes to 315 minutes, almost an hour more.</p>
<p>What did I have to show for these additional hours of tagging and training? Here are my final values for the training and validation loss:</p>
<div class="wide">
  <canvas id="loss"></canvas>
</div>
<script>
  window.hvynck_charts["loss"] = {
    type: 'bar',
    data: {
      labels: [
        'Original',
        'Simple Tags',
        'Complex Tags',
      ],
      datasets: [
        {
          label: "Training",
          backgroundColor:
		    window.hvynck_clrs.BLUE,
          data: [ 4.2471, 4.3200, 4.2976 ],
        },
		{
		  label: "Validation",
		  backgroundColor:
			window.hvynck_clrs.RED,
		  data: [ 4.211433, 4.254242, 4.254888 ],
		},
      ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
      tooltips: {mode: 'nearest', intersect: false, },
      hover: { mode: 'nearest', intersect: false, },
      title: {
        display: false,
        text: 'Loss',
      },
      scales: {
        yAxes: [{
          scaleLabel: { 
            display: true,
            labelString: "Loss",
            padding: 0,
          }, 
		  ticks: {min: 4},}],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Model</th><th>Training Loss</th><th>Validation Loss</th></tr>
<tr><td>Original</td><td>4.2471</td><td>4.2114</td></tr>
<tr><td>Simple Tags</td><td>4.3200</td><td>4.2542</td></tr>
<tr><td>Complex Tags</td><td>4.2976</td><td>4.2548</td></tr>
</table>
</noscript>
<p>And here is the validation <a href="https://en.m.wikipedia.org/wiki/Perplexity">perplexity</a>:</p>
<div class="wide">
  <canvas id="perplexity"></canvas>
</div>
<script>
  window.hvynck_charts["perplexity"] = {
    type: 'bar',
    data: {
      labels: [
        'Original',
        'Simple Tags',
        'Complex Tags',
      ],
      datasets: [{
        label: "Perplexity",
        backgroundColor: [
          window.hvynck_clrs.RED,
		  window.hvynck_clrs.BLUE,
		  window.hvynck_clrs.YELLOW,
        ],
        data: [ 67.453131, 70.403431, 70.448926 ],
      },],
    },
    options: {
      responsive: true,
      legend: { display: false, },
      tooltips: {mode: 'nearest', intersect: false, },
      hover: { mode: 'nearest', intersect: false, },
      title: {
        display: false,
        text: 'Perplexity',
      },
      scales: {
        yAxes: [{
          scaleLabel: { 
            display: true,
            labelString: "Perplexity",
            padding: 0,
          }, 
		  ticks: {min: 0},}],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Model</th><th>Perplexity</th></tr>
<tr><td>Original</td><td>67.4531</td></tr>
<tr><td>Simple Tags</td><td>70.4034</td></tr>
<tr><td>Complex Tags</td><td>70.4489</td></tr>
</table>
</noscript>
<p>There’s nothing too surprising there. The task of predicting the next word <em>and</em> its part of speech is harder than predicting just the next word. A larger vocabulary is harder to model. Given the same amount of training time, the system gets a lower score on the more complex task.</p>
<h2 id="theeffectonthesentimentdetection">The Effect on the Sentiment Detection</h2>
<p>This will be a very short section. It’s pretty easy to sum up: almost none. Which is disappointing, I have to admit. In all cases, training took around 1 minute per epoch. Also in all cases, the accuracy is about <code>93%</code>. Across all three cases, the range of the final accuracy is about <code>0.2%</code>, which I think is well within the margin of error. I’m not even going to bother charting it. The version with the simple tags scored lowest, the version one the complex tags scored highest. But the differences are so small that I suspect small changes in the distribution between training and validation sets would remove them. Given the random initialisation, there’s even a good chance that just repeating the experiment would give a different ordering.</p>
<h2 id="analysis">Analysis</h2>
<p>On the one hand: this didn’t make things any better, on the other: it didn’t make them any worse. The fact that the model which uses the parts of speech can do worse at the language modelling stage but still get the same score for sentiment detection is pretty interesting.</p>
<p>Howard notes in the lecture that once the training schedule in the notebook is complete the network isn’t really close to overfitting. If anything, I think the modified versions are even further away. There’s still lots of room to train all three versions of the model.</p>
<p>I think that’s most the likely the best approach to carry forwards:</p>
<ol>
<li>Train the language modelling system almost to overfitting;</li>
<li>Train the sentiment detection almost to overfitting;</li>
</ol>
<p>Then repeat this this with the versions which incorporate the part of speech tags. Once the three models are fully trained, a better comparison can probably be made.</p>
<p>Thus far I’ve been doing all of the training inside a Jupytor notebooks. That’s great for interactivity, but highly suboptimal for long lived training. If the browser tab crashes or loses its connection to the kernel hours of work can be lost. So if I do take another run at this I’ll probably also use it as a reason to learn how to use <a href="https://www.paperspace.com/gradient">Paperspace’s Gradient</a> system, which allows long lived jobs to be run remotely<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>.</p>
<p>Discuss this post on <a href="https://news.ycombinator.com/item?id=17588025">Hacker News</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>The sub-course in question is actually called “sequence models”. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Apparently this is called a  <a href="https://www.merriam-webster.com/words-at-play/words-own-opposites">“Janus Word”</a> or contronym. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>This sort of things sometimes freaks me out about Python. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>It’s very similar to how <a href="https://www.floydhub.com">Floydhub</a> works, as far as I can tell. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Kaggle’s Yelp Restaurant Photo Classification Competition, Fast.ai Style: Part 2]]></title><description><![CDATA[Hacks, improvements and graphs. Lots of graphs. Part 2 of my attempt to grapple with the Kaggle Yelp Restaurant Photo Classification competition, using the techniques (and code library) from fast.ai’s “Practical Deep Learning for Coders” course.]]></description><link>https://harveynick.com/2018/06/30/kaggles-yelp-restaurant-photo-classification-competition-fast-ai-style-part-2/</link><guid isPermaLink="false">5b3738cf5e18e300bf36c805</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sat, 30 Jun 2018 09:35:20 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Note: This post has a lot of javascript graphs, but if you’re using a feed reader or have javascript turned off you’ll just get basic tables. Sorry about that.</p>
<p>With that out of the way: I’m going to assume that if you’re reading this you’ve already read <a href="https://harveynick.com/2018/06/24/kaggles-yelp-restaurant-photo-classification-competition-fast-ai-style-part-1/">Part 1</a>. As such, I’m just going to dive right back in where I left off.</p>
<h1 id="calculatingtheperbusinessf1onthefly">Calculating the Per Business F1 on the Fly</h1>
<p>Having now calculated the per business F1 at the end of the training run, I realised it would be useful (or at least interesting) to be able to see how it was changing during training. The per photo F1 makes for a decent heuristic, but isn’t guaranteed to actually correlate with the per business F1, which is what I actually care about.</p>
<p>I hit another snag here with the the fast.ai library. At the end of each epoch, the metrics supplied by the user are calculated using the same batch size as training. The results for each batch are then averaged, and this is what is shown to the user.</p>
<p>That’s a problem for calculating the per business F1, as the entire dataset is needed to build the per business predictions. As a smaller and less obvious problem: it also means that the per photo F1 which is displayed at the end of the epoch will be even more inflated than I thought. This is because rather than finding the best threshold for the entire data set, a more specialised threshold will be found for each batch. It’s overfitting, essentially. The premature optimisation of the machine learning world.</p>
<p>I could solve both problems if I could collate the per photo predications before calculating the F1. If I had access to state which persisted between batches, I could use something like the method below to collate the predictions and target values.</p>
<pre><code class="language-python">def collate(preds, targs, data):
  multiplier = 1.0
  if preds.shape[0] != len(photo_idx_to_val_biz_idx):
    if data['preds'] is None:
      data['preds'] = preds
      data['targs'] = targs
      # The dataset is incomplete.
      return 0, None, None
    # Append the data to the known data.
    data['preds'] = torch.cat([data['preds'], preds])
    data['targs'] = torch.cat([data['targs'], targs])
    if data['preds'].shape[0] != len(photo_idx_to_val_biz_idx):
      # The dataset is incomplete.
      return 0, None, None
    # The dataset is complete.
    # See below for an explanation of the multiplier.
    multiplier = len(photo_idx_to_val_biz_idx) / preds.shape[0]
    preds = data['preds']
    targs = data['targs']
    data['preds'] = None
    data['targs'] = None
  return multiplier, preds, targs
</code></pre>
<p>That’s all well and good, but the metric calculations are supplied to the fast.ai library as a pure function, which is then run without additional state or context. Except… this is Python, and calling something a “pure” function is <a href="https://imgur.com/gallery/mSHi8">a sign, <em>not</em> a cop</a>. Functions in Python are first class objects, and objects in Python have arbitrarily assignable state. So there is a way around this problem…</p>
<p>Warning: If the Python code I admitted to using elsewhere in these posts bothers you, that below will almost certainly bother you even more. And it should. It’s a horrible hack, and nothing like it should ever get anywhere near a production environment. It should never even get near the critical path of a <em>non</em> production environment. But still. I’m not using it for either of those things.</p>
<pre><code class="language-python">def f1_biz_avg(preds, targs, start=0.24, end=0.50, step=0.01):
  # Collate the predictions and targets, using persistent
  # state attached to this function.
  multiplier, preds, targs = collate(preds, targs, f1_biz_avg.data)

  # A multiplier of 0 means that collation is incomplete.
  if multiplier == 0.0:
    return 0
    
  # Convert the per photo values to per business values.
  biz_preds, biz_targs = photo_to_biz(preds, targs, True)

  # Ignore warnings.
  with warnings.catch_warnings():
    warnings.simplefilter(&quot;ignore&quot;)

        # Find the threshold which yields the best F1.
    mapping = {th : f1_score(biz_targs, (biz_preds &gt; th), average='samples')
           for th in np.arange(start,end,step)}
    th = max(mapping.keys(), key=mapping.get)

    # Return the best F1, scaled so that the fast.ai library
    # will average it with the 0's to get the correct value. 
    return mapping[th] * multiplier

# Initialize the persistent state.
f1_biz_avg.data = {'preds': None, 'targs': None}
</code></pre>
<p>This works around two issues:</p>
<ol>
<li>Collating the predications before calculating the F1;</li>
<li>Scaling the output of the final batch so that when it’s averaged with the zeros returned for the other batches the correct value results.</li>
</ol>
<p>I’ll say it again: all of this is a horrible hack. I’m ashamed of it. I worry that if anyone who works for my employer sees this, I might be fired. And yet...</p>
<p><img src="http://images.harveynick.com/2018-06-29-kaggles-yelp-restaurant-part-2-1-proud_meme.jpeg" alt=""></p>
<p>This is how the per photo F1 compares to the business F1 over the course of the training schedule:</p>
<div class="wide">
  <canvas id="f1photovsbiz"></canvas>
</div>
<script>
  window.hvynck_charts["f1photovsbiz"] = {
    type: 'line',
    data: {
      labels: [
        ['1', '1'], ['2'], ['3'], ['4'], ['5'],
		['1', '2'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
		['1', '3'], ['2'], ['3'], ['4'], ['5'],
		['1', '4'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
      ],
      datasets: [
		{
		label: 'Per Photo F1',
		borderColor: window.hvynck_clrs.RED,
		backgroundColor: window.hvynck_clrs.RED,
		fill: false,
		data: [ 
		  0.7119, 0.7261, 0.7307, 0.7324, 0.7336,
		  0.7357, 0.7413, 0.7421, 0.7448, 0.7466, 0.7473, null,
		  0.7485, 0.7493, 0.7495, 0.7491, 0.7497,
		  0.7516, 0.7537, 0.7533, 0.7554, 0.7562, 0.7563, 0.7565,],
	  	},
		{
		label: 'Per Business F1',
		borderColor: window.hvynck_clrs.BLUE,
		backgroundColor: window.hvynck_clrs.BLUE,
		fill: false,
		data: [ 
		  0.7422, 0.7352, 0.7374, 0.7394, 0.7369,
		  0.7422, 0.7500, 0.7543, 0.7610, 0.7723, 0.7647, null,
		  0.7618, 0.7686, 0.7682, 0.7641, 0.7663,
		  0.7776, 0.7760, 0.7766, 0.7856, 0.7807, 0.7874, 0.7845,],
	  	},
	  ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
      tooltips: {mode: 'index', intersect: false, },
      hover: { mode: 'index', intersect: false, },
      title: {
        display: false,
      },
      scales: {
        yAxes: [
          {
            scaleLabel: { 
              display: true,
			  labelString: "F1 Score",
              padding: 0,
            },
          },
        ],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Set</th><th>Epoch</th><th>Per Photo F1</th><th>Per Business F1</th></tr>
<tr><td>1</td><td>1</td><td>0.711957</td><td>0.74221</td></tr>
<tr><td></td><td>2</td><td>0.726165</td><td>0.735293</td></tr>
<tr><td></td><td>3</td><td>0.730785</td><td>0.737406</td></tr>
<tr><td></td><td>4</td><td>0.732486</td><td>0.739411</td></tr>
<tr><td></td><td>5</td><td>0.733644</td><td>0.736971</td></tr>
<tr><td>2</td><td>1</td><td>0.73573</td><td>0.742263</td></tr>
<tr><td></td><td>2</td><td>0.741325</td><td>0.750094</td></tr>
<tr><td></td><td>3</td><td>0.742113</td><td>0.754373</td></tr>
<tr><td></td><td>4</td><td>0.744895</td><td>0.761078</td></tr>
<tr><td></td><td>5</td><td>0.746609</td><td>0.77235</td></tr>
<tr><td></td><td>6</td><td>0.74738</td><td>0.76478</td></tr>
<tr><td></td><td>7</td><td></td><td></td></tr>
<tr><td>3</td><td>1</td><td>0.748527</td><td>0.761866</td></tr>
<tr><td></td><td>2</td><td>0.74936</td><td>0.768682</td></tr>
<tr><td></td><td>3</td><td>0.749511</td><td>0.768273</td></tr>
<tr><td></td><td>4</td><td>0.749174</td><td>0.764132</td></tr>
<tr><td></td><td>5</td><td>0.749718</td><td>0.766357</td></tr>
<tr><td>4</td><td>1</td><td>0.751661</td><td>0.777684</td></tr>
<tr><td></td><td>2</td><td>0.753739</td><td>0.776017</td></tr>
<tr><td></td><td>3</td><td>0.753325</td><td>0.776626</td></tr>
<tr><td></td><td>4</td><td>0.755435</td><td>0.785654</td></tr>
<tr><td></td><td>5</td><td>0.756214</td><td>0.780722</td></tr>
<tr><td></td><td>6</td><td>0.756346</td><td>0.787445</td></tr>
<tr><td></td><td>7</td><td>0.756536</td><td>0.784503</td></tr>
</table>
</noscript>
<p>As you can see the per business F1 is consistently higher than the per photo, but less stable. The latter makes sense, given that the model is being trained against the individual photos, not the business. The former was a little surprising to me. I assume the mixed signals start to cancel each other out when you average the predications together.</p>
<h2 id="comparingdifferentarchitectures">Comparing Different Architectures</h2>
<p>An F1 of <code>0.7845</code> was actually pretty close to my original goal, but not quite there. The obvious next step was to try the same approach with a more advanced model. I also thought it might be interesting to compare the performance of a few different CNN architectures for my own information. So next I ran the exact same schedule, but using the <a href="https://www.kaggle.com/pytorch/resnet50">ResNet-50</a> and <a href="https://github.com/facebookresearch/ResNeXt">ResNext-50</a> CNN architectures.</p>
<div class="wide">
  <canvas id="archcomp"></canvas>
</div>
<script>
  window.hvynck_charts["archcomp"] = {
    type: 'line',
    data: {
      labels: [
        ['1', '1'], ['2'], ['3'], ['4'], ['5'],
		['1', '2'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
		['1', '3'], ['2'], ['3'], ['4'], ['5'],
		['1', '4'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
      ],
      datasets: [
		{
		label: 'ResNet-34',
		borderColor: window.hvynck_clrs.RED,
		backgroundColor: window.hvynck_clrs.RED,
		fill: false,
		data: [ 
		  0.7422, 0.7352, 0.7374, 0.7394, 0.7369,
		  0.7422, 0.7500, 0.7543, 0.7610, 0.7723, 0.7647, null,
		  0.7618, 0.7686, 0.7682, 0.7641, 0.7663,
		  0.7776, 0.7760, 0.7766, 0.7856, 0.7807, 0.7874, 0.7845,],
	  	},
		{
		label: 'ResNet-50',
		borderColor: window.hvynck_clrs.BLUE,
		backgroundColor: window.hvynck_clrs.BLUE,
		fill: false,
		data: [ 
		  0.7870, 0.7775, 0.7720, 0.7739, 0.7692,
		  0.7632, 0.7752, 0.7811, 0.7824, 0.7863, 0.7911, 0.7913,
		  0.7906, 0.8002, 0.7865, 0.7893, 0.7992,
		  0.8054, 0.8020, 0.8061, 0.8053, 0.8021, 0.8085, 0.8082,],
	  	},
		{
		label: 'ResNext-50',
		borderColor: window.hvynck_clrs.YELLOW,
		backgroundColor: window.hvynck_clrs.YELLOW,
		fill: false,
		data: [ 
		  0.7919, 0.7828, 0.7645, 0.7686, 0.7676,
		  0.7793, 0.7888, 0.7882, 0.7942, 0.7996, 0.7998, 0.7976,
		  0.7936, 0.8000, 0.8020, 0.7944, 0.7976,
		  0.8021, 0.7995, 0.8051, 0.8069, 0.8045, 0.8068, 0.8078,],
	  	},
	  ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
      tooltips: {mode: 'index', intersect: false, },
      hover: { mode: 'index', intersect: false, },
      title: {
        display: false,
      },
      scales: {
        yAxes: [
          {
            scaleLabel: { 
              display: true,
			  labelString: "F1 Score",
              padding: 0,
            },
          },
        ],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Set</th><th>Epoch</th><th>resnet34</th><th>resnet50</th><th>resnext50</th></tr>
<tr><td>1</td><td>1</td><td>0.711957</td><td>0.727451</td><td>0.72667</td></tr>
<tr><td></td><td>2</td><td>0.726165</td><td>0.73852</td><td>0.737242</td></tr>
<tr><td></td><td>3</td><td>0.730785</td><td>0.742298</td><td>0.740598</td></tr>
<tr><td></td><td>4</td><td>0.732486</td><td>0.744425</td><td>0.742447</td></tr>
<tr><td></td><td>5</td><td>0.733644</td><td>0.745335</td><td>0.743586</td></tr>
<tr><td>2</td><td>1</td><td>0.73573</td><td>0.745172</td><td>0.747723</td></tr>
<tr><td></td><td>2</td><td>0.741325</td><td>0.750352</td><td>0.753836</td></tr>
<tr><td></td><td>3</td><td>0.742113</td><td>0.751288</td><td>0.754403</td></tr>
<tr><td></td><td>4</td><td>0.744895</td><td>0.754363</td><td>0.75765</td></tr>
<tr><td></td><td>5</td><td>0.746609</td><td>0.75604</td><td>0.759276</td></tr>
<tr><td></td><td>6</td><td>0.74738</td><td>0.756608</td><td>0.759684</td></tr>
<tr><td></td><td>7</td><td></td><td>0.75674</td><td>0.760055</td></tr>
<tr><td>3</td><td>1</td><td>0.748527</td><td>0.758205</td><td>0.761691</td></tr>
<tr><td></td><td>2</td><td>0.74936</td><td>0.758833</td><td>0.762266</td></tr>
<tr><td></td><td>3</td><td>0.749511</td><td>0.758572</td><td>0.762637</td></tr>
<tr><td></td><td>4</td><td>0.749174</td><td>0.759194</td><td>0.762198</td></tr>
<tr><td></td><td>5</td><td>0.749718</td><td>0.759701</td><td>0.762108</td></tr>
<tr><td>4</td><td>1</td><td>0.751661</td><td>0.761914</td><td>0.761366</td></tr>
<tr><td></td><td>2</td><td>0.753739</td><td>0.763467</td><td>0.762696</td></tr>
<tr><td></td><td>3</td><td>0.753325</td><td>0.762945</td><td>0.762683</td></tr>
<tr><td></td><td>4</td><td>0.755435</td><td>0.764565</td><td>0.76418</td></tr>
<tr><td></td><td>5</td><td>0.756214</td><td>0.764523</td><td>0.764647</td></tr>
<tr><td></td><td>6</td><td>0.756346</td><td>0.765443</td><td>0.764441</td></tr>
<tr><td></td><td>7</td><td>0.756536</td><td>0.765464</td><td>0.765044</td></tr>
</table>
</noscript>
I was pretty sure that both 50 layer architectures would do consistently better than ResNet-34, but I also thought that ResNext-50 would do consistently better than ResNet-50. So I was half right.
<p>One advantage ResNext-50 did have is that it trained more quickly. Stupidly, I didn’t record the training time for each architecture. I trained ResNet-34 over the course of a day. I’d say that ResNet-50 look about half as long again to train as ResNet-34<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. ResNext-50 seemed like it took about halfway between the two. But that could be my imagination. Next time I should actually record the timings…</p>
<p>Be that as it may, my initial goal was to achieve an F1 score of at least <code>0.8</code> against the validation set. <code>0.8082</code> is (just barely) higher than that, so: mission accomplished, I guess.</p>
<p>Right?</p>
<h2 id="tryingclassspecificthresholds">Trying Class Specific Thresholds</h2>
<p>At this point I started to realise a few things which would have been obvious at the top if I had more experience. Firstly, after I started writing this post I realised it might be interesting to graph the proportion of businesses which belong to each class. For your convenience (and in the interest of making the following charts readable on mobile), here are the class names again:</p>
<ol start="0">
<li>Good for lunch;</li>
<li>Good for dinner;</li>
<li>Takes reservations;</li>
<li>Outdoor seating;</li>
<li>Restaurant is expensive;</li>
<li>Has alcohol;</li>
<li>Has table service;</li>
<li>Ambience is classy;</li>
<li>Good for kids.</li>
</ol>
<p>And here is a graph of their proportions:</p>
<div class="wide">
  <canvas id="proportion"></canvas>
</div>
<script>
  window.hvynck_charts["proportion"] = {
    type: 'bar',
    data: {
      labels: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', 'Mean' ],
      datasets: [{
		borderColor: window.hvynck_clrs.BLUE,
		backgroundColor: window.hvynck_clrs.BLUE,
        data: [ 0.3300, 0.5125, 0.5275, 0.5125, 0.2625, 0.6375, 0.6850, 0.2825, 0.6075, 0.4842 ],
      },],
    },
    options: {
      responsive: true,
      legend: { display: false, },
      tooltips: {mode: 'nearest', intersect: false, },
      hover: { mode: 'nearest', intersect: false, },
      title: {
        display: false,
        text: 'Proportion of Businesses with each Class',
      },
      scales: {
        yAxes: [
          {
            scaleLabel: { 
              display: true,
              labelString: "Proportion",
              padding: 0,
            },
            ticks: {min: 0, max: 1},
          },
        ],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Class</th><th>Proportion</th></tr>
<tr><td>good_for_lunch</td><td>0.3300</td></tr>
<tr><td>good_for_dinner</td><td>0.5125</td></tr>
<tr><td>takes_reservations</td><td>0.5275</td></tr>
<tr><td>outdoor_seating</td><td>0.5125</td></tr>
<tr><td>restaurant_is_expensive</td><td>0.2625</td></tr>
<tr><td>has_alcohol</td><td>0.6375</td></tr>
<tr><td>has_table_service</td><td>0.6850</td></tr>
<tr><td>ambience_is_classy</td><td>0.2825</td></tr>
<tr><td>good_for_kids</td><td>0.6075</td></tr>
<tr><td>average</td><td>0.4842</td></tr>
</table>
</noscript>
<p>Following on from this, I started to wonder whether my system was doing better on some classes rather than others. Which is when the obvious thought arrived: I was using the same threshold for each class, but I had no reason to assume that the sensitivity was the same. I could probably get better results by using different thresholds for each class.</p>
<p>I ran the following code against the inference output for the validation set to find the best individual threshold for for each class:</p>
<pre><code class="language-python">def per_class_threshholds(preds, targs, start=0.04, end=0.50, step=0.001):

  # Initialize the per class threshholds to 0.
  thresholds = np.zeros((preds.shape[1]))

  # Ignore warnings.
  with warnings.catch_warnings():
    warnings.simplefilter(&quot;ignore&quot;)
  
    # Iterate 10 times, trying to improve the thresholds each
    # time.
    # Note: This is overkill, but runs quickly enough not to matter.
    # Some CPU time could be saved by stopping once the F1 is no 
    # longer improving.
    for _ in range(10):
      # Try to improve the threshold for each class in turn.
      for i in range(0, thresholds[0]):
        best_th = 0.0
        best_score = 0.0
        for th in np.arange(start, end, step):
          thresholds[i] = th
          score = f1_score(targs, (preds &gt; thresholds), average='samples')
          if score &gt; best_score:
            best_th = th
            best_score = score
        thresholds[i] = best_th

  return thresholds
</code></pre>
<p>Running this for each of three architectures individually gave me the per class F1 scores. You can see them in the graph below, which I’ve foreshortened to emphasise the differences in performance:</p>
<div class="wide">
  <canvas id="perclassf1"></canvas>
</div>
<script>
  window.hvynck_charts["perclassf1"] = {
    type: 'bar',
    data: {
      labels: [ '0', '1', '2', '3', '4', '5', '6', '7', '8', 'Mean'],
      datasets: [
        {
	  	  label: 'ResNet-34',
		  borderColor: window.hvynck_clrs.RED,
		  backgroundColor: window.hvynck_clrs.RED,
          data: [ 0.6824, 0.8326, 0.8728, 0.6944, 0.7510, 0.8905,
                  0.9252, 0.7672, 0.8716, 0.8097 ],
        },
		{
	  	  label: 'ResNet-50',
		  borderColor: window.hvynck_clrs.BLUE,
		  backgroundColor: window.hvynck_clrs.BLUE,
		  data: [ 0.6643, 0.8387, 0.8874, 0.7449, 0.7401, 0.9136,
	              0.9329, 0.7679, 0.8835, 0.8193 ],
		},
		{
	  	  label: 'ResNext-50',
		  borderColor: window.hvynck_clrs.YELLOW,
		  backgroundColor: window.hvynck_clrs.YELLOW,
		  data: [ 0.6053, 0.8372, 0.8805, 0.7329, 0.7570, 0.8869, 
                  0.9310, 0.7857, 0.8793, 0.8106 ],
		},
      ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
	  tooltips: {mode: 'index', intersect: false, },
	  hover: { mode: 'index', intersect: false, },
      title: {
        display: false,
        text: 'Proportion of Businesses with each Class',
      },
      scales: {
        yAxes: [
          {
            scaleLabel: { 
              display: true,
			  labelString: "F1 Score",
              padding: 0,
            },
            ticks: {min: 0.5, max: 1},
          },
        ],
      },
    }
  };
</script>
<noscript>
<table>
<tr><th>Class</th><th>resnet34 F1</th><th>resnet50 F1</th><th>resnext50 F1</th><th>Ensemble F1</th></tr>
<tr><td>good_for_lunch</td><td>0.6824</td><td>0.6643</td><td>0.6053</td><td>0.6824</td></tr>
<tr><td>good_for_dinner</td><td>0.8326</td><td>0.8387</td><td>0.8372</td><td>0.8387</td></tr>
<tr><td>takes_reservations</td><td>0.8728</td><td>0.8874</td><td>0.8805</td><td>0.8874</td></tr>
<tr><td>outdoor_seating</td><td>0.6944</td><td>0.7449</td><td>0.7329</td><td>0.7449</td></tr>
<tr><td>restaurant_is_expensive</td><td>0.7510</td><td>0.7401</td><td>0.7570</td><td>0.7570</td></tr>
<tr><td>has_alcohol</td><td>0.8905</td><td>0.9136</td><td>0.8869</td><td>0.9136</td></tr>
<tr><td>has_table_service</td><td>0.9252</td><td>0.9329</td><td>0.9310</td><td>0.9329</td></tr>
<tr><td>ambience_is_classy</td><td>0.7672</td><td>0.7679</td><td>0.7857</td><td>0.7857</td></tr>
<tr><td>good_for_kids</td><td>0.8716</td><td>0.8835</td><td>0.8793</td><td>0.8835</td></tr>
<tr><td>average</td><td>0.8097</td><td>0.8193</td><td>0.8106</td><td>0.8251</td></tr>
</table>
</noscript>
<p>There’s actually more variation than I was expecting. Firstly between the per class scores. There’s some correlation with the per class proportions above, but not for every class. Accounting for that effect, “Good for lunch” and “takes reservations” appear to be the hardest classes to detect.</p>
<p>Secondly the best architecture is not consistent across the classes. ResNet-34 is actually a bit of a dark horse when it comes to detecting establishments which open for lunch. Who knew?</p>
<p>Speaking of ideas which occur to you after the fact: I’m willing to bet that the time stamp of the photo is probably a pretty solid signal for the “good for lunch” class.</p>
<p>At this point I didn’t trust the overall F1 scores these thresholds gave me against the validation set. It was time run against the test set, submit to Kaggle, and find out what my real score was. I did this for each of the architectures, and also built an ensemble output by using the predictions of the architecture which got the best score for each class.</p>
<p>Having run inference on the test data, I used the following code to build per business predications<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> and generate the formatted output.</p>
<pre><code class="language-python">predications = # The per photo predictions.
test_photo_to_biz = f'{PATH}/test_photo_to_biz.csv'
test_photo_to_biz_data = pd.read_csv(test_photo_to_biz)

# Gather the individual business IDs, and the image counts
# for each business.
biz_counts = {}
for biz_id in test_photo_to_biz_data.business_id:
  biz_counts.setdefault(biz_id, 0)
  biz_counts[biz_id] += 1
biz_ids = list(biz_counts.keys())
biz_idxs = {biz_ids[i] : i for i in range(len(biz_ids))}

# Extract the photo IDs in order from the test image file names.
images_in_order = [v[9:-4] for v in learn.data.test_ds.fnames]
photo_idxs = {int(images_in_order[i]) : i for i in range(len(images_in_order))}

# Convert the per photo predictions to per business predictions.
biz_preds = np.zeros((len(biz_ids), preds2.shape[1]))
for i in range(test_photo_to_biz_data.shape[0]):
  photo_id = test_photo_to_biz_data.photo_id[i]
  photo_idx = photo_idxs[photo_id]
  photo_preds = predications[photo_idx, :]
  
  biz_id = test_photo_to_biz_data.business_id[i]
  biz_idx = biz_idxs[biz_id]
  biz_count = biz_counts[biz_id]
  
  biz_preds[biz_idx, :] += photo_preds * (1.0 / biz_count)

# Convert the predications into booleans.
biz_cls = preds &gt; threshholds

# Convert the booleans into lists of matched classes.
classes = []
for i in range(biz_cls.shape[0]):
  biz_cls_biz = biz_cls[i, :]
  biz_classes = &quot; &quot;.join([str(i) for i in range(preds_cls.shape[1]) if biz_cls_biz[i]])
  classes.append(biz_classes)

# Build a pandas data frame with the business IDs and
# matched classes.
data = np.array(list(zip(biz_ids, classes)), order = 'F')
output = pd.DataFrame(data=data, columns=['business_id', 'labels'])
# Write the data frame out to a CSV files.
csv_fn=f'{PATH}tmp/sub_{f_model.__name__}.csv'
output.to_csv(csv_fn, index=False)
# Display a link to the CSV file.
FileLink(csv_fn)
</code></pre>
<p>The code used to build the ensemble is left as an exercise for the reader. Obviously this is for educational purposes. Not just because I don’t want you to see <em>my</em> code and possibly judge me more harshly than you already do for the other code in this post. Ahem.</p>
<p>So, without further ado, here are the final scores against the public and private leaderboards for the competition:</p>
<div class="wide">
  <canvas id="leaderboard"></canvas>
</div>
<script>
  window.hvynck_charts["leaderboard"] = {
    type: 'bar',
    data: {
      labels: [
        'Public Score',
		'Private Score',
      ],
      datasets: [
        {
	  	  label: 'ResNet-34',
		  borderColor: window.hvynck_clrs.RED,
		  backgroundColor: window.hvynck_clrs.RED,
          data: [ 0.7788, 0.78823, ],
        },
		{
	  	  label: 'ResNet-50',
		  borderColor: window.hvynck_clrs.BLUE,
		  backgroundColor: window.hvynck_clrs.BLUE,
		  data: [ 0.8009, 0.8062, ],
		},
		{
	  	  label: 'ResNext-50',
		  borderColor: window.hvynck_clrs.YELLOW,
		  backgroundColor: window.hvynck_clrs.YELLOW,
		  data: [ 0.7872, 0.7830, ],
		},
		{
	  	  label: 'Ensemble',
		  borderColor: window.hvynck_clrs.GREEN,
		  backgroundColor: window.hvynck_clrs.GREEN,
		  data: [ 0.7896, 0.8001, ],
		},
      ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
	  tooltips: {mode: 'index', intersect: false, },
	  hover: { mode: 'index', intersect: false, },
      title: {
        display: false,
        text: 'Final F1 Scores',
      },
	  scales: {
		yAxes: [
		  {
			scaleLabel: { 
			  display: true,
			  labelString: "F1 Score",
			  padding: 0,
			},
			ticks: {min: 0.77, max: 0.82},
		  },
		],
	  },
    }
  };
</script>
<noscript>
<table>
<tr><th>Model</th><th>Public Score</th><th>Private Score</th></tr>
<tr><td>ResNet-34</td><td>0.7788</td><td>0.78823</td></tr>
<tr><td>ResNet-50</td><td>0.8009</td><td>0.8062</td></tr>
<tr><td>ResNext-50</td><td>0.7872</td><td>0.7830</td></tr>
<tr><td>Ensemble</td><td>0.7896</td><td>0.8001</td></tr>
<table>
</table></table></noscript>
<p>As you can see, ResNet-50 wins bother leaderboards pretty handily. That’s not a huge surprise, but I really wasn’t expecting ResNet-34 to beat ResNext-50 on the private leaderboard. The ensemble takes a respectable second place on both leaderboards. I would tend to blame overfitting for it not coming first. Overfitting is <a href="https://en.wikipedia.org/wiki/Ernst_Stavro_Blofeld">Blofeld</a>. Absent another obvious villain, it’s usually the culprit.</p>
<p>Regardless, I’m definitely over my target F1 score of <code>0.8</code>. Enough over it to get me inside the top 100 on the private leaderboard. Which would put me on the bronze podium. You know… if this competition hadn’t ended two years ago…</p>
<h2 id="ideasforfurtherimprovement">Ideas for Further Improvement</h2>
<p>At this stage I have essentially four ideas to try and improve on these results.</p>
<p>The first is simple: try exactly the same approach with a more complex CNN architecture, such as <a href="https://www.kaggle.com/pytorch/resnet101">ResNet-101</a>. The reason I haven’t tried this one already is also simple: time. I already had to train ResNet-50 and ResNext-50 across multiple days. At a rough guess I’d expect ResNet-101 to take twice as long. If I was actually entering a competition here I would probably try it, but since I’m not it doesn’t seem worth the effort. There’s also no guarantee it would actually improve the results. It might just overfit.</p>
<p>Idea two is to tweak the loss function in order to minimise (if not eliminate) the mixed signals it currently sends out. My first thought was to use the standard multi-class loss functions, but de-emphasise or remove the term which punishes false negatives. That would probably lead to a model which just returns false for every class, though. Another thought I had was to add an additional class “none of the above” and then use a single class loss function with no punishment of false negatives. The model would then have to select a single class for each image, or explicitly select that none apply. Of course this would only work if there is at least one image which provides a good representation of each class for each business. As for why I haven’t tried this yet: I’m not experienced enough with PyTorch (which the fast.ai library is based on) and I don’t know how. Yet.</p>
<p>My third idea is to fiddle with the data loader. Each training example would become a business, rather than a photo. When loading the data, the data loader would randomly select (say) 4 of the images for that restaurant and return a composite of them. So it might return the following image (or any other permutation) for business <code>485</code>, which I used as an example in <a href="https://harveynick.com/2018/06/24/kaggles-yelp-restaurant-photo-classification-competition-fast-ai-style-part-1/">Part 1</a>:</p>
<p><img src="http://images.harveynick.com/2018-06-29-kaggles-yelp-restaurant-part-2-2-composite.jpg" alt=""></p>
<p>Idea four isn’t actually my idea at all, but one which came from Hacker News user <a href="https://news.ycombinator.com/user?id=kaveh_h">kaveh_h</a> (aka Kaveh Hadjar) in <a href="https://news.ycombinator.com/item?id=17390896">this comment</a>. It amounts to this: use a recurrent neural network (such as an <a href="https://en.wikipedia.org/wiki/Recurrent_neural_network#LSTM">LSTM</a>) in place of the fully connected classification layers of the model. You would then batch together all of the images for a restaurant, run them all through the network sequentially, and then train / infer based on the final output. It would look something like this (art style shamelessly stolen from <a href="https://stratechery.com">Stratechery</a>):</p>
<p><img src="http://images.harveynick.com/2018-06-29-kaggles-yelp-restaurant-part-2-3-extension.jpeg" alt="wide"></p>
<p>RNNs are ideal for handling sequences of data, so there is some possibility that the order the images were supplied in could make a difference. I also have absolutely no idea how I’d go about putting something like that together, either using the fast.ai library or without it.</p>
<p>In summery: I have a lot to learn. Which is quite exciting, to be honest.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Probably about <code>50/34</code> times as long, in fact. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>The photo to business prediction code I wrote for the validation set no longer works here. Thankfully something much similar suffices. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Kaggle’s Yelp Restaurant Photo Classification Competition, Fast.ai Style: Part 1]]></title><description><![CDATA[Part 1 of my attempt to grapple with the Kaggle Yelp Restaurant Photo Classification competition, using the techniques (and code library) from fast.ai’s “Practical Deep Learning for Coders” course.]]></description><link>https://harveynick.com/2018/06/24/kaggles-yelp-restaurant-photo-classification-competition-fast-ai-style-part-1/</link><guid isPermaLink="false">5b2fdcc76d815400bfa5c2a3</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 24 Jun 2018 18:07:13 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Lectures 3 and 4 of fast.ai’s <a href="http://course.fast.ai">Practical Deep Learning for Coders</a> MOOC focuses in part on multi-label image classification. Teacher <a href="https://en.wikipedia.org/wiki/Jeremy_Howard_(entrepreneur)">Jeremy Howard</a> uses the <a href="https://www.kaggle.com/c/planet-understanding-the-amazon-from-space">Understanding the Amazon from Space</a> Kaggle competition for teaching purposes<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, and sets homework to try other similar image classification competitions.</p>
<p>The forums point to a template version of the <a href="http://jupyter.org">Jupyter</a> notebook used in the lecture, which suggests trying the <a href="https://www.kaggle.com/c/yelp-restaurant-photo-classification">Yelp Restaurant Photo Classification</a> competition. On the surface this actually turned out to be a pretty suboptimal match for the techniques used in the lecture and the default setup of the <a href="https://github.com/fastai/fastai">fast.ai</a> library. But it did give me an opportunity to dig a little deeper than I might have otherwise.</p>
<p>Howard’s guidance is that students should aim to get an evaluation score which would put them in the top 50% of the leaderboard for the completion. Taking a look at the <a href="https://www.kaggle.com/c/yelp-restaurant-photo-classification/leaderboard">leaderboard</a> I decided to aim slightly higher: I wanted to get an <a href="https://en.wikipedia.org/wiki/F1_score">F1 score</a> of at least <code>0.8</code>. Ideally this score would be against the test set, but if I managed it against the validation set I’d be happy enough with that.</p>
<h2 id="specificsofthecompetition">Specifics of the Competition</h2>
<p>This competition has a degree of separation between the input and the expected output. To explain what I mean by that, let’s compare the training data from the Yelp completion to that from the Amazon competition. In both cases the data is supplied as jpeg images and <a href="https://en.wikipedia.org/wiki/Comma-separated_values">CSV files</a>. For the Amazon competition there is a single CSV file, mapping images to labels. It looks like this:</p>
<pre><code>image_name,tags
train_0,haze primary
train_1,agriculture clear primary water
train_2,clear primary
train_3,clear primary
train_4,agriculture clear habitation primary road
train_5,haze primary water
</code></pre>
<p>Nice and simple. The name of the image on the first column; the labels for the image in the second. Mapping images of satellite imagery to appropriate labels is, after all, the point of this competition. For the yelp competition there are two CSV files. The first maps <em>businesses</em> (not images) to labels:</p>
<pre><code>business_id,labels
1000,1 2 3 4 5 6 7
1001,0 1 6 8
100,1 2 4 5 6 7
...
485,1 2 3 4 5 6 7
...
</code></pre>
<p>Again this represents the point of the completion. We’re trying to learn the right labels for a particular restaurant. The images are a data tool we use in order to do so. So, there is a <em>second</em> CSV file which maps images to businesses:</p>
<pre><code>photo_id,business_id
204149,3034
52779,2805
278973,485
195284,485
19992,485
80748,485
...
</code></pre>
<p>This is the degree of separation: no direct mapping between the input data (the images) and the desired output (the labels).</p>
<p>This presents two main problems. The first is small: the data needs to be merged into a format which can be used to train a neural network. Solving this leads to the second, much bigger issue: many of the resulting label to image mappings are inappropriate. But there isn’t enough information in the data set to do anything other than map every label for a business to every image for that business.</p>
<p>Consider that there are 9 labels:</p>
<ol start="0">
<li>Good for lunch;</li>
<li>Good for dinner;</li>
<li>Takes reservations;</li>
<li>Outdoor seating;</li>
<li>Restaurant is expensive;</li>
<li>Has alcohol;</li>
<li>Has table service;</li>
<li>Ambience is classy;</li>
<li>Good for kids.</li>
</ol>
<p>Now consider business <code>485</code> from the data above. It has every label apart form 0 (good for lunch) and 8 (good for kids). Associated with it are these 4 images:</p>
<p><img src="http://images.harveynick.com/2018-06-24-kaggles-yelp-restaurant-part-1-1-278973.jpg" alt=""><br>
<img src="http://images.harveynick.com/2018-06-24-kaggles-yelp-restaurant-part-1-2-195284.jpg" alt=""><br>
<img src="http://images.harveynick.com/2018-06-24-kaggles-yelp-restaurant-part-1-3-19992.jpg" alt=""><br>
<img src="http://images.harveynick.com/2018-06-24-kaggles-yelp-restaurant-part-1-4-80748.jpg" alt=""></p>
<p>Do each of those images demonstrate each of those labels? I can certainly see that the the presence of wine glasses in the last suggests that alcoholic drinks are available. But there’s nothing in any of the other three pictures which suggests booze is on the menu to my eyes. Likewise I’m not sure the third image suggests any of the labels, yet in training it will be expected to match all of them.</p>
<p>That’s not all. From the description of the data:</p>
<blockquote>
<p>Since Yelp is a community driven website, there are duplicated images in the dataset. They are mainly due to:</p>
<ol>
<li>users accidentally upload the same photo to the same business more than once (e.g., <a href="https://www.yelp.com/biz_photos/ria-mar-south-river?select=lq_9e9LSxt6PXMYGcO-tFw">this</a> and <a href="https://www.yelp.com/biz_photos/ria-mar-south-river?select=Z1qaoescegy0ImS3rNvvRA">this</a>)</li>
<li>chain businesses which upload the same photo to different branches<br>
Yelp is including these as part of the competition, since these are challenges Yelp researchers face every day.</li>
</ol>
</blockquote>
<p>So the same image might be in the training set multiple times, with entirely different labels each time. That’s a lot of mixed signals.</p>
<p>The upshot of this is that the problem is harder. This is borne out by the leaderboard results for the two competitions. The winning score for the Amazon competition is <code>0.93317</code> and 100th place has <code>0.92895</code>. The winning score for the Yelp competition, however, is <code>0.83177</code>, with 100th place getting <code>0.80087</code>. Now, I want to stress that this is an apples to oranges comparison. The Yelp competition is graded using the F1 score, whereas the Amazon competition uses the F2 score, which punishes false negatives more harshly. Nevertheless, that’s a big difference and a larger drop off between 1st and 100th place.</p>
<p>Harder doesn’t mean impossible, though. I was curious as to whether the fast.ai techniques would work anyway. Beyond that I wondered if there was anything I could tweak to make them work better.</p>
<h2 id="processingtheinputandpickingthevalidationset">Processing the Input and Picking the Validation Set</h2>
<p>I originally screwed this up and wasted a good few hours of training. The crux of the matter is this: your validation set should be based on the individual restaurants, not the individual images. I know that the first time around, but I didn’t fully understand the way the fast.ai library would handle it. The following is how I built my second, <em>correct</em> validation set.</p>
<p>Side note: I did (and continue to) do all my work for the fast.ai course using the fast.ai template at Paperspace, which I can highly recommend. If you want to try it our you can use my referral code to get $5 credit <a href="https://www.paperspace.com/&amp;R=AAGWLUH">here</a>.</p>
<p>First things first, I set the paths for the input CSV files and loaded them into <a href="https://pandas.pydata.org">pandas</a> data frames:</p>
<pre><code class="language-python">PATH = 'data/yelp/'
photo_to_biz = f'{PATH}/train_photo_to_biz_ids.csv'
biz_to_labels = f'{PATH}/train.csv'
photo_to_biz_data = pd.read_csv(photo_to_biz)
biz_to_labels_data = pd.read_csv(biz_to_labels)
</code></pre>
<p>Next I selected the businesses which will be used for validation using fast.ai’s <code>get_cv_idxs</code> method. This provides a random but deterministic<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> list of indices given a dataset size. I added a new column to the <code>biz_to_labels_data</code> data frame and set it to true for every business in the validation set.</p>
<pre><code class="language-python">val_biz_idxs = get_cv_idxs(biz_to_labels_data.shape[0])
val_biz_idxs_set = set(val_biz_idxs)

for index in range(biz_to_labels_data.shape[0]):
	biz_to_labels_data.loc[index, 'validation_set'] = index in val_biz_idxs_set
</code></pre>
<p>You specify the validation set to the fast.ai library by giving it a list of the indices in the data set which are to be used for validation. But these indices must be based on the on-disk order of the input files, <em>not</em> the order they appear in the input CSV. Remember above when I said that I originally messed up the validation set? This point about how the fast.ai library interprets the validation set indices is where I did it. I didn’t look deeply enough at my original validation set, and that cost me a lot of time.</p>
<p>I joined the two data frames on the <code>business_id</code> field. Then sorted the resulting data frame by <code>photo_id</code>.  As the <code>photo_id</code> field corresponds to the filename of each image, sorting on it means the two orders are now the same. This done, the indices of the validation data can be found by including the row number of each item which has the <code>validation_set</code> column I created above set to <code>True</code>.</p>
<pre><code class="language-python">joined = pd.merge(photo_to_biz_data, biz_to_labels_data, on='business_id')
joined.sort_values(by='photo_id', inplace=True)
val_idxs = [i for i in range(joined.shape[0])
            if joined.iloc[i, -1]]
</code></pre>
<p>Finally what remains is to output just the <code>photo_id</code> and <code>labels</code> columns to a new CSV which can be read in by the fast.ai library:</p>
<pre><code class="language-python">photos_to_labels = f'{PATH}/train_photos_to_labels.csv'
joined.to_csv(photos_to_labels, columns=['photo_id', 'labels'],
			  index=False)
</code></pre>
<p>A key thing I learned here is that I need more experience with pandas. I’m pretty sure there are much more elegant and idiomatic ways of achieving the above. In the lecture, Howard recommends <a href="https://amzn.to/2Jdsv8T">Python for Data Analysis</a> which is written by the main author of pandas. That’s going on my todo list.</p>
<h2 id="firstrunsthroughresnet34">First Runs Through ResNet-34</h2>
<p>I’m not going to go too deep into the nuts and bolts of actually training the neural network, nor talk about finding the learning rates. You can find pretty comprehensive notes and code samples for this in the fast.ai course forum <a href="http://forums.fast.ai/t/wiki-lesson-3/9401">here</a>.</p>
<p>There are a few things which <a href="https://harveynick.com/2018/05/07/some-notes-on-courseras-andrew-ng-deep-learning-speciality/">Andrew Ng’s Coursera Deep Learning Specialisation</a> treats as advanced topics, but fast.ai bakes in from the outset. One of these is <a href="https://en.wikipedia.org/wiki/Transfer_learning">transfer learning</a>. The starting point as taught by fast.ai is to use the <a href="https://www.kaggle.com/pytorch/resnet34">ResNet-34</a> architecture with weights pre-trained against the <a href="http://www.image-net.org">ImageNet</a> dataset. The trained weights are kept for the convolutional layers, but new fully connected classification layers are added to the end. Following the fast.ai recipe, I trained the new layers for 5 epochs, keeping the weights of the convolutional layers static. Then I unfroze the weights of the convolutional layers and continued training for a total of 7 epochs<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>Something included in fast.ai from the start but not present in Andrew Ng’s course at all is one of Howard’s tricks for avoiding overfitting. This comes now. I increased the size of the input images from 244px to 299px then repeated the the above procedure. This makes the full regime:</p>
<ol>
<li>5 epochs with an image size of 224px and the convolutional layers frozen;</li>
<li>7 epochs with an image size of 224px and the convolutional layers unfrozen;</li>
<li>5 epochs with an image size of 299px and the convolutional layers frozen;</li>
<li>7 epochs with an image size of 299px and the convolutional layers unfrozen;</li>
</ol>
<p>Why 244px and 299px? It’s mentioned in the lectures that these are the standard sizes of images in the ImageNet dataset, which the ResNet was trained against. When I originally started playing with the data I tried a three stage progression from 64px to 128px to 256px, but found I was getting much better results more quickly by going directly to 244px and 299px. This may or may not be the case for other datasets. Figuring it out is definitely an art. I think Rick put it best:</p>
<p><img src="http://images.harveynick.com/2018-06-24-kaggles-yelp-restaurant-part-1-5-sometimes_science.jpg" alt=""></p>
<p>The fast.ai library allows you to supply additional metrics when you train the network. These are entirely for the user’s feedback, and have no affect on the training itself. In order to get a better handle on how the training was actually going, I put together a function which returns the best case F1 value by picking the most effective decision boundary:</p>
<pre><code class="language-python">def f1(preds, targs, start=0.17, end=0.50, step=0.01):

	# Ignore warnings.
	with warnings.catch_warnings():
		warnings.simplefilter(&quot;ignore&quot;)

		# Find the threshold which yields the best F1.
		# Note: np.arange(...) is essentially range(...) for floats.
		mapping = {th : f1_score(targs, (preds &gt; th), average='samples')
				   for th in np.arange(start,end,step)}
		th = max(mapping.keys(), key=mapping.get)
		
		# Return the F1 generated by this threshold.
		return mapping[th]
</code></pre>
<p>Running ResNet-34 with the above schedule gave me the following values for the trading and validation losses, plus my highly optimistic per photo F1 metric.</p>
<div class="wide">
  <canvas id="resnet34loss"></canvas>
</div>
<script>
  window.hvynck_charts["resnet34loss"] = {
    type: 'line',
    data: {
      labels: [
        ['1', '1'], ['2'], ['3'], ['4'], ['5'],
		['1', '2'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
		['1', '3'], ['2'], ['3'], ['4'], ['5'],
		['1', '4'], ['2'], ['3'], ['4'], ['5'], ['6'], ['7'],
      ],
      datasets: [
		{
        label: 'Training Loss',
        borderColor: window.hvynck_clrs.RED,
		backgroundColor: window.hvynck_clrs.RED,
		fill: false,
        data: [
			0.6189, 0.5754, 0.5513, 0.5483, 0.5383,
			0.5350, 0.5161, 0.5163, 0.5089, 0.5006, 0.4904, null,
			0.4994, 0.4895, 0.4861, 0.4896, 0.4854,
			0.4831, 0.4777, 0.4849, 0.4744, 0.4750, 0.4693, 0.4756,
		],
      	},
		{
		label: 'Validation Loss',
		borderColor: window.hvynck_clrs.BLUE,
		backgroundColor: window.hvynck_clrs.BLUE,
		fill: false,
		data: [ 
			0.5872, 0.5551, 0.5452, 0.5409, 0.5368,
			0.5356, 0.5239, 0.5222, 0.5165, 0.5146, 0.5115, null,
			0.5094, 0.5093, 0.5088, 0.5079, 0.5070,
			0.5047, 0.5006, 0.5010, 0.4979, 0.4958, 0.4958, 0.4956,
 		],
	  	},
		{
		label: 'Per Photo F1',
		borderColor: window.hvynck_clrs.YELLOW,
		backgroundColor: window.hvynck_clrs.YELLOW,
		fill: false,
		data: [ 
		  0.7119, 0.7261, 0.7307, 0.7324, 0.7336,
		  0.7357, 0.7413, 0.7421, 0.7448, 0.7466, 0.7473, null,
		  0.7485, 0.7493, 0.7495, 0.7491, 0.7497,
		  0.7516, 0.7537, 0.7533, 0.7554, 0.7562, 0.7563, 0.7565,],
	  	},
	  ],
    },
    options: {
      responsive: true,
      legend: { display: true, },
      tooltips: {mode: 'index', intersect: false, },
      hover: { mode: 'index', intersect: false, },
      title: {
        display: false,
      },
      scales: {
        yAxes: [
          {
            scaleLabel: { 
              display: true,
              labelString: "Loss / F1",
              padding: 0,
            },
          },
        ],
      },
    }
  };
</script>
<noscript>
<table>
<th></th><td>Set</td><td>Epoch</td><td>Training Loss</td><td>Validation Loss</td><td>F1</td>
<tr><td>1</td><td>1</td><td>0.618955</td><td>0.587201</td><td>0.711957</td></tr>
<tr><td></td><td>2</td><td>0.575491</td><td>0.555127</td><td>0.726165</td></tr>
<tr><td></td><td>3</td><td>0.551347</td><td>0.5452</td><td>0.730785</td></tr>
<tr><td></td><td>4</td><td>0.548335</td><td>0.540938</td><td>0.732486</td></tr>
<tr><td></td><td>5</td><td>0.538313</td><td>0.536894</td><td>0.733644</td></tr>
<tr><td>2</td><td>1</td><td>0.535068</td><td>0.535683</td><td>0.73573</td></tr>
<tr><td></td><td>2</td><td>0.516119</td><td>0.523915</td><td>0.741325</td></tr>
<tr><td></td><td>3</td><td>0.516364</td><td>0.522218</td><td>0.742113</td></tr>
<tr><td></td><td>4</td><td>0.508947</td><td>0.51653</td><td>0.744895</td></tr>
<tr><td></td><td>5</td><td>0.500656</td><td>0.514619</td><td>0.746609</td></tr>
<tr><td></td><td>6</td><td>0.490499</td><td>0.511588</td><td>0.74738</td></tr>
<tr><td></td><td>7</td><td></td><td></td><td></td></tr>
<tr><td>3</td><td>1</td><td>0.499408</td><td>0.509486</td><td>0.748527</td></tr>
<tr><td></td><td>2</td><td>0.489586</td><td>0.509366</td><td>0.74936</td></tr>
<tr><td></td><td>3</td><td>0.486146</td><td>0.5088</td><td>0.749511</td></tr>
<tr><td></td><td>4</td><td>0.489602</td><td>0.507902</td><td>0.749174</td></tr>
<tr><td></td><td>5</td><td>0.485432</td><td>0.507049</td><td>0.749718</td></tr>
<tr><td>4</td><td>1</td><td>0.483158</td><td>0.504774</td><td>0.751661</td></tr>
<tr><td></td><td>2</td><td>0.477729</td><td>0.500665</td><td>0.753739</td></tr>
<tr><td></td><td>3</td><td>0.484929</td><td>0.501044</td><td>0.753325</td></tr>
<tr><td></td><td>4</td><td>0.47441</td><td>0.497946</td><td>0.755435</td></tr>
<tr><td></td><td>5</td><td>0.475028</td><td>0.495898</td><td>0.756214</td></tr>
<tr><td></td><td>6</td><td>0.469361</td><td>0.495856</td><td>0.756346</td></tr>
<tr><td></td><td>7</td><td>0.475626</td><td>0.495669</td><td>0.756536</td></tr>
</table>
</noscript>
<p>You’ll notice that there’s a data point missing at the end of the second set of epochs. The Jupyter notebook had a bit of an issue here, and though the training finished successfully, the loss and metric output didn’t make it to the screen. Frustrating, but this is one of the dangers of using Jupyter for long lived training runs.</p>
<h2 id="processingtheoutput">Processing the Output</h2>
<p>With the training runs finished, the next step was to test against in the validation set. At this stage I need per business, rather than per photo, F1. More processing is needed.</p>
<p>Remember before when I said my use of pandas was far from elegant and idiomatic? Well… look away now if that bothered you, because it’s about to get a lot worse. One of the dangers of Python is that it’s really easy to use it as a write only language. You can put a lot of power into a single line of code which makes no sense to you about an hour later.</p>
<p>Well... my quickly hacked together solution for matching photos to businesses in the validation set is one of those times. It uses a series of three dictionary comprehensions to to map the index of each photo in the validation <em>photo</em> set to the index of the appropriate business in the validation <em>business</em> set.</p>
<pre><code class="language-python"># Map the ids of businesses in the business validation set to their
# index in that set. 
val_biz_ids = {biz_to_labels_data.loc[val_biz_idxs[i], 'business_id'] : i
			   for i in range(len(val_biz_idxs))}
# Map the ids of photos in the photo validation set to their index
# in that set.
val_photo_ids = {joined.iloc[val_idxs[i], -4] : i for i in range(len(val_idxs))}

# Map index in the photo validation set to index in the business validation
# set.
photo_idx_to_val_biz_idx = {val_photo_ids[joined.iloc[i, -4]] :
							val_biz_ids[joined.iloc[i, -3]] for i in val_idxs}
</code></pre>
<p>With that done, I wrote a new method which first builds the per business predictions. I originally tried two approaches to this: taking the maximum of the predicted values for each class; and taking the mean of the predicted values. After a little bit of experimentation, I found that the mean<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup> gave better results.</p>
<pre><code class="language-python">def photo_to_biz(preds, targs):
	
	# Initial storage for predications and targets.
	biz_preds = np.zeros((len(val_biz_idxs), preds.shape[1]))
	biz_targs = np.zeros((len(val_biz_idxs), targs.shape[1]))
	# Counts of the number of photos observed for each business.
	# Used to calculate a rolling average.
	biz_counts = {}
	
	for val_idx in range(preds.shape[0]):
		biz_idx = photo_idx_to_val_biz_idx[val_idx]
		
		# Update the number of photos seen for this business.
		biz_count = biz_counts.get(biz_idx, 0) + 1
		biz_counts[biz_idx] = biz_count

		# Update the rolling mean of the predictions.
		frac = ((biz_count-1) / biz_count)
		biz_preds[biz_idx,:] = (biz_preds[biz_idx,:] * frac) + (preds[val_idx,:] / biz_count)
		
		# Use max to update the target values.
		# (Technically this only needs to be done once for each
		# business and could be precalculated).
		biz_targs[biz_idx,:] = np.maximum(biz_targs[biz_idx,:], targs[val_idx,:])

	return biz_preds, biz_targs
</code></pre>
<p>The output can then be fed into the F1 calculation above. Surprisingly (to me), the per-business F1 score actually same out higher than the per photo score. <code>0.7845</code> vs <code>0.7565</code>, which is a notable improvement.</p>
<p>Not quite good enough to hit my goal, though.</p>
<p>Next time: Dirty hacks, improvements galore, submitting to Kaggle, and graphs. Lots of graphs. You can read it <a href="https://harveynick.com/2018/06/30/kaggles-yelp-restaurant-photo-classification-competition-fast-ai-style-part-2/">here</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>As he is fond of doing. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Meaning that it always returns the same same output given the same input. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>It’s actually more complicated that than, but as I noted above: that’s not important right now. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Again, this might not be the case for other datasets. I used a rolling calculation of the mean. This code could be made a little simpler by pre-counting the number of photos for each business. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[A Short Word About GDPR]]></title><description><![CDATA[How this site will be handling changes required by the GDPR. Plus an aside regarding targeting types in Google AdSense.]]></description><link>https://harveynick.com/2018/05/28/a-short-word-about-gdpr/</link><guid isPermaLink="false">5b0adb2402c89100bf0d55b5</guid><category><![CDATA[Blogger]]></category><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Mon, 28 May 2018 14:11:48 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>If you’re not dead and you’ve been on the internet recently, you’ve probably head about GDPR. Given the circumstances of you reading this article, I’m going to assume that both of those things are true. <a href="https://ec.europa.eu/commission/priorities/justice-and-fundamental-rights/data-protection/2018-reform-eu-data-protection-rules_en">GDPR</a>, which of course stands for <del>Google Democratic People’s Republic</del> General Data Protection Regulation, is a new law in the EU. The gist of it is this: you’re data belongs to you; you get to choose who has it; you get to choose how it is used.</p>
<p>Without fear of breaking my NDA, I think I can say that GDPR led to a lot of internal effort at Google. Perhaps not quite as much as you would think. Google does genuinely try to be a good steward of its users’ data. But still, there was effort, and possibly the biggest nexus around which that effort revolved is Google’s advertising business.</p>
<p>Most of the commentary on GDPR amounted to it being an annoyance for large businesses and a burden for smaller ones. Some smaller companies have <a href="https://gdprhallofshame.com/14-you-have-been-terminated/">stopped doing business</a> in the EU permanently (or so they say). Some lightbulbs have stopped working in the EU (<a href="https://gdprhallofshame.com/7-sorry-your-light-bulbs-and-gdpr-dont-work/">not kidding</a>). Some US news websites are <a href="https://gdprhallofshame.com/13-what-if-we-did-nothing/">currently blocked</a><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, and USA today actually <a href="https://twitter.com/fr3ino/status/1000166112615714816?s=19">threw up</a> an EU only ad free version of the site<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<p>There is also some effect on small blogs such as this one. At least: some effect on small blogs such as this one… which happen to use use Google AdSense. Google has actually made it the publisher’s responsibility to get consent for showing personalised advertising. Why would a publisher want to do that? Well, let me illustrate by showing the “tracking type” distribution for the tiny amount of AdSense revenue I’ve made via this site:</p>
<div class="wide">
  <canvas id="targetting"></canvas>
</div>
<script>
  var barChartData = {
    labels: [
      'Personalised',
      'Contextual',
      'Run of Network',
      'None',
      'Placement',
    ],
    datasets: [{
      label: 'Earnings',
      backgroundColor: [
        'rgb(255, 99, 132)',
        'rgb(54, 162, 235)',
        'rgb(255, 205, 86)',
        'rgb(75, 192, 192)',
        'rgb(201, 203, 207)'],
        data: [ 4.44, 3.03, 0.04, 0.01, 0.00 ],
    },]
  };
  window.onload = function() {
    var ctx = document.getElementById('targetting').getContext('2d');
    new Chart(ctx, {
      type: 'bar',
      data: barChartData,
      options: {
        responsive: true,
        legend: { display: false, },
        tooltips: {mode: 'nearest', intersect: false, },
        hover: { mode: 'nearest', intersect: false, },
        title: {
            display: true,
            text: 'Earnings by Targetting Type',
        },
        scales: {
          yAxes: [{ scaleLabel: { 
            display: true,
            labelString: "GBP (£)",
            padding: 0,
          }, }],
        },
      }
    });
  };
</script>
<noscript>
  <table>
    <tr> <th>Targetting Type</th> <th>Estimated Earnings</th> </tr>
    <tr> <td>Personalised</td>    <td>£4.44</td> </tr>
    <tr> <td>Contextual</td>      <td>£3.03</td> </tr>
    <tr> <td>Run of Network</td>  <td>£0.04</td> </tr>
    <tr> <td>None</td>            <td>£0.01</td> </tr>
    <tr> <td>Placement</td>       <td>£0.00</td> </tr>
  </table>
</noscript>
<p>As you can see well over half of this tiny amount of money has come from personalised ads<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. If this was my main source of income you could see why I might want to keep showing ads with personalised targeting. It isn’t, though. Also: in order to keep showing them I’d have to ask each user if they’re okay with it. Then I’ve have to store that information somewhere, probably with a cookie. I do not want any part of <a href="https://ico.org.uk/for-organisations/guide-to-pecr/cookies-and-similar-technologies/">using cookies</a>.</p>
<p>Where I’m going with this, dear reader, is that I’ve gone ahead and made the decision on your behalf. Sincere apologies if you do wish to be tracked, and see adverts which are more relevant to you on all of the websites you visit. However I’ve decided that actually I’d prefer the readers of this site not be tracked. So, as per Google’s instructions, I’ve added the following line of code to the ad unit which is shown on this site:</p>
<pre><code class="language-js">(adsbygoogle=window.adsbygoogle||[]).requestNonPersonalizedAds=1
</code></pre>
<p>The upshot of this is that you should never see personalised ads when reading this site.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>What the hell are you doing with our data, guys? <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>It’s wonderful. Just HTML and CSS. I wish this is what all news websites were like. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Personalised and contextual targeting are somewhat straightforward. “Run of network” is remarking (you put these shoes in your basket but didn’t buy them), I think. Placement means someone specifically placed an ad on my site. I don’t have that enabled (I think?), so it makes sense that it’s zero. “None” is a puzzling one, though. It’s not included in the <a href="https://support.google.com/adsense/answer/1055502?hl=en-GB">AdSense definitions</a>. I guess there are circumstances under which an ad is selected randomly?! <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[An iOS Developer’s Opinions of Flutter]]></title><description><![CDATA[A lengthy discussion of the Flutter cross platform UI framework and my feelings about it. TLDR: I like the Flutter framework a lot, but: I wish it used Swift as its programming language; and I find its facsimile of native OS components to be imperfect.]]></description><link>https://harveynick.com/2018/05/21/an-ios-developers-opinions-of-flutter/</link><guid isPermaLink="false">5b01cb16b5880a00bfc7dbda</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Mon, 21 May 2018 17:18:24 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Around a year ago I led a team which spent several months building a complete app using <a href="https://flutter.io">Flutter</a>. For various reasons (none of which I can go into) this app did not ship, and I moved on to a different role. More recently, I spent a week participating in a <a href="https://en.m.wikipedia.org/wiki/Hackathon">hackathon</a>. The team I was part of built a successful proof of concept inside an existing app using Flutter.</p>
<p>As a result, Flutter is kind of on my mind right now. I’d like to take the opportunity to put my thoughts down in words. As it turns out, my thoughts on this are quite lengthy. If two and a half thousand words sounds like too much, you can find the TLDR above.</p>
<h2 id="whoexactlyami">Who Exactly am I?</h2>
<p>Since I’m expressing an opinion, I think it’s worth laying out my experience in relevant areas. Presently I’m mostly a front end web developer at Google, where (at the time of writing) I have been employed for almost exactly 7 years. As such the current tools of my trade are JavaScript (using Google’s <a href="https://developers.google.com/closure/">Closure</a> compiler), CSS and HTML.</p>
<p>I originally started my Google career as a backend developer. About a year into that I incepted, coded, and shipped the <a href="https://itunes.apple.com/us/app/google-adsense/id680739529?mt=8">Google AdSense App for iPhone</a>. Up until the most recent releases (which has an additional contributer<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>) I was responsible for every line of (non-library) code in the app. After that I moved to the Google Calendar team and helped ship the v1 of <a href="https://itunes.apple.com/us/app/google-calendar/id909319292?mt=8">Google Calendar for iOS</a>. At the time I left that team I was responsible for the architecture of about 80% of the UI, and all of the animations in the app. I also worked on several of Google’s internal UI libraries for iOS. The last time I looked I had contributed around 500k lines of <a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html">Objective-C</a> to Google’s codebase.</p>
<p>I have scant experience of coding for Android, but at <em>lot</em> of experience of using the Java programming language, having previously been a backend developer, and using it extensively during my PhD.</p>
<p>On the whole I enjoy coding for iOS a great deal. In fact I still play with coding iOS Apps in <a href="https://developer.apple.com/swift/">Swift</a> during my spare time. I enjoy web development less, and Android development much less still. I also enjoy science fiction TV shows and American style barbecue, but that’s not important right now.</p>
<h2 id="whatisflutter">What is Flutter?</h2>
<p>Flutter is essentially two things:</p>
<ol>
<li>A cross platform framework which allows you to build an app once and then ship it on both Android and iOS;</li>
<li>The UI framework used by Google’s in development <a href="https://en.m.wikipedia.org/wiki/Google_Fuchsia">Fuchsia</a> operating system.</li>
</ol>
<p>You could probably make the case that it’s actually a cross platform UI framework which allows you build for three operating systems, including one which doesn’t entirely exist yet.</p>
<p>To talk about it more deeply I’m going to break it down into three subareas: the programming language, the APIs, and the renderer. Small warning before we start: this is going to be something of a reverse shit sandwich. Perhaps not quite that extreme, but the filling is definitely a lot better than the bread.</p>
<h3 id="anatomyofflutterpart1thedartprogramminglanguage">Anatomy of Flutter Part 1: The Dart Programming Language</h3>
<p><a href="https://www.dartlang.org">Dart</a> was conceived at Google as, essentially, <a href="http://radar.oreilly.com/2012/03/what-is-dart.html">a better JavaScript</a>. It was intended to replace JavaScript as the web language of choice. Thus its design goals included that it should:</p>
<ul>
<li>run on a reasonably sized virtual machine (or VM);</li>
<li><a href="https://en.m.wikipedia.org/wiki/Source-to-source_compiler">transpile</a> it to sensible JavaScript for use in browsers which lacked a Dart VM;</li>
<li>appeal to JavaScript programmers.</li>
</ul>
<p>Unfortunately, that “browsers which lack a Dart VM” part is what killed the original plan. The one browser the Dart team thought they could count on, Google Chrome, decided that it was already a little on the heavy side. The last thing it needed was to included a <em>second</em> virtual machine, especially on mobile platforms<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<p>That didn’t actually kill the use of the language, though. The “transpile to JavaScript” aspect remained alive, and it evolved into a peer of <a href="http://www.typescriptlang.org">TypeScript</a> and <a href="http://coffeescript.org">CoffeeScript</a>. It remains in active use both inside and outside of Google.</p>
<p>Originating as a planned alternative to JavaScript, in its original form it was extremely dynamic. Type annotations were an option the programmer could chose to add. Later more concrete static typing was added in the form of “strong mode”, which become the default as of the <a href="https://www.dartlang.org/dart-2">2.0 release</a>.</p>
<p>I would describe Dart as boring, but in a good way. It’s neat, productive, and pleasant to use. These are the only exciting things about it. Again: this is not a bad thing. Aside from some very neat <a href="https://www.dartlang.org/guides/language/language-tour#constructors">constructor syntax</a> and perhaps the <a href="https://www.dartlang.org/guides/language/language-tour#cascade-notation-">cascade operator</a>, you are unlikely to be surprised by the content of a Dart codebase.</p>
<p>When I move from Swift to Dart I find myself missing Swift’s <a href="https://developer.apple.com/documentation/swift/optional">optionals</a>, <a href="https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Enumerations.html">enums</a>, <a href="https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html">value types</a> and <a href="https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html">weak references</a>. On the other hand, Dart has language level asynchronous functionality in the form of <a href="https://www.dartlang.org/guides/language/language-tour#asynchrony-support">async/await</a>, which is currently only a <a href="https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619">proposal</a> for Swift. I think Dart also has a shallower learning curve than Swift, as there is less to learn.</p>
<p>I would say I prefer Swift to Dart, but I prefer Dart to Java. I <em>definitely</em> prefer Dart to JavaScript. Dart is... fine. It’s a totally solid choice for a UI language. If you have a Dart based web codebase you can even share some business logic, in theory<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>I have no <em>direct</em> knowledge of this, but: it’s also a solid political choice. Using Java as the Android programming language has caused Google <a href="https://en.m.wikipedia.org/wiki/Oracle_America,_Inc._v._Google,_Inc.">more than a little bit of trouble</a>. I can see why a language which is both open source <em>and</em> stewarded by Google directly makes sense here.</p>
<h3 id="anatomyofflutterpart2theflutterframework">Anatomy of Flutter Part 2: The Flutter Framework</h3>
<p>The Flutter UI APIs are very different to both <a href="https://developer.apple.com/documentation/uikit">UIKit</a> and the <a href="https://developer.android.com/guide/platform/">Android framework</a>, being completely declarative in native. In Flutter you build your UI as an immutable tree of widgets, branches of which are then rebuilt in response to changes in state.</p>
<p>Consider a simple static text label. Using Swift and iOS that would look something like this:</p>
<pre><code>let label = UILabel()
label.text = &quot;Hello world!&quot;
</code></pre>
<p>In Flutter it would look quite similar:</p>
<pre><code>final label = Text(&quot;Hello world!&quot;);
</code></pre>
<p>The differences become more obvious when we want the displayed text to change. Here’s one way of doing that in Swift:</p>
<pre><code>let label = UILabel()
label.text = &quot;Hello world!&quot;

func setText(text: String) {
  label.text = text
}
</code></pre>
<p>UIKit’s labels are mutable, making this very easy. Flutter’s <code>Text</code> widget is stateless and immutable, though. To update its value we’d need to wrap it in a <code>StatefulWidget</code> and do something like this:</p>
<pre><code>class TextWidget extends StatefulWidget {

  @override
  TextWidgetState createState() =&gt; TextWidgetState();
}

class TextWidgetState extends State&lt;TextWidget&gt; {
  String text = &quot;hello world&quot;;

  void setText(String text) {
    // setState takes a function which updates the local state
    // as input, then rebuilds the tree from this point down.
    setState(() {
      this.text = text;
    });
  }

  @override
  Widget build(BuildContext context) =&gt; Text(text);
}
</code></pre>
<p>This is imperfect, however. There’s no good way to get hold of that setter on the state object. Instead we need to handle the change reactively, using something like a Dart <a href="https://www.dartlang.org/tutorials/language/streams"><code>Stream</code></a>:</p>
<pre><code>class TextWidget extends StatefulWidget {
  final Stream&lt;String&gt; textStream;

  // This is the neat constructor syntax I mentioned before.
  TextWidget(this.textStream);

  @override
  TextWidgetState createState() =&gt; TextWidgetState();
}

class TextWidgetState extends State&lt;TextWidget&gt; {
  String text = &quot;hello world&quot;;

  @override
  void initState() {
    super.initState();
    // This will call [setText] whenever the contents of [textStream] changes.
    widget.textStream.listen(setText);
  }

  void setText(String text) {
    // [setState] takes a function which updates the local state
    // as input, then rebuilds the tree from this point down.
    setState(() {
      this.text = text;
    });
  }

  @override
  Widget build(BuildContext context) =&gt; Text(text);
}
</code></pre>
<p>Perhaps a better Swift comparison would be to use <a href="https://github.com/ReactiveX/RxSwift">rxSwift</a>. That would look something like this:</p>
<pre><code>let label = UILabel()
label.text = &quot;Hello world!&quot;
let textObservable: Observable&lt;String?&gt;
textObservable.bindTo(label.rx.text)
</code></pre>
<p>In either case: Flutter requires more code for these examples, but that’s because I’m leaving out some of the additional code which would be needed for UIKit. The Flutter is self contained, and it makes the state transforms much more explicit. In the UIKit example, anything which can get a reference to the label can modify its state. Not so for Flutter. The <code>Stream</code> is the only means by which the text can be updated. The <code>setState</code> method is the only means by which the local state and child widgets of a <code>StatefulWidget</code> can be updated in turn. That removes whole classes of bugs.</p>
<p>I’ve barely scratched the surface here. You can find much deeper and more complete explanations of how Flutter widgets work <a href="https://flutter.io/widgets-intro/">on the official website</a>. There are also specific guides for <a href="https://flutter.io/flutter-for-android/">Android</a>, <a href="https://flutter.io/flutter-for-ios/">iOS</a> and <a href="https://flutter.io/web-analogs/">Web</a> developers.</p>
<p>For the most part, I really like this approach to building apps. Some things (infinite scrolling, for example) can be quite hard to achieve with the built-in widgets. I’ve also found a few apparently simple layouts which required complex solutions with Flutter. But here’s where Flutter has a strong advantage: it’s <a href="https://github.com/flutter/flutter">open source all the way down</a>. If one of the default widgets doesn’t quite do what you want, you can just fork the code and adjust it to meet your needs.</p>
<p>I find Flutter to be incredibly productive. I’m amazed at what the small team I worked with over the last week managed to get done. Across my experience of working with Flutter, I’ve also found that it does tend to produce fewer UI bugs. Where as UIKit pushes you towards an <a href="https://en.m.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller">MVC</a> approach to building an app, Flutter pushes you towards a <a href="https://en.m.wikipedia.org/wiki/Reactive_programming">reactive</a> approach. This reduces the number of ways data can flow through the app, and makes it easier to reason about.</p>
<p>Comparing it to UIKit directly, I would say Flutter is close to on-par in terms of API quality. It is, however, significantly less comprehensive. This is hardly surprising. iOS and UIKit within it will ship its 12.0 version this year.  Flutter has not yet hit 1.0.</p>
<h3 id="anatomyofflutterpart3theflutterrenderer">Anatomy of Flutter Part 3: The Flutter Renderer</h3>
<p>Now we get to the more controversial aspect of Flutter. That part which makes some mobile developers exclaim “What?! Gross!”</p>
<p>In my experience, people tend to put Flutter into the same mental box as <a href="https://facebook.github.io/react-native/">React Native</a>. Both are cross platform and both use what are usually thought of as “web languages”. Both strongly push users towards a reactive approach to data flow (obviously). That’s more or less where the comparisons end, however. React Native UIs use the system’s native components. So UIKit on iOS and Android Framework on Android. Flutter does not. It has its own OpenGL based renderer, and <a href="https://flutter.io/faq/#does-flutter-use-my-systems-oem-widgets">creates its own UI components</a> completely from scratch. It handles user interaction directly, and has its own gesture handlers.</p>
<p>Within this sandbox, it emulates the look and feel of the host operating system user interface. In fact, there is a Flutter demo which allows the look and feel to be “flipped”, using iOS iconography and physics on Android and vice versa.</p>
<p>Here is one of the downsides of Flutter: In my opinion, and when built for iOS, this facsimile is imperfect. The UI looks just a <em>little</em> off. The scroll physics aren’t quite right. Animations don’t move in quite the way I would expect. It’s climbing the cliff at the other side of the uncanny valley, but not quite out of it. I have less experience of Android, so find it hard to make a comparison there. But I’m told it does a much better job<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>.</p>
<p>Another point which I think is worth bringing up: Unless you make a specific effort to have it be otherwise, an app built with Flutter will look almost identical on iOS and Android. Flutter will make only minimal changes (e.g. default font, back button icon, title justification) by default. There are the <a href="https://flutter.io/widgets/cupertino/">“Cupertino” widgets</a> which mimic the iOS design language, built they lag behind iOS. In fact at the time of writing they do not appear to be up to date with iOS 11 which is more than 6 months old. That being the case: you’re probably looking at shipping a Material Design app if you use Flutter. Now, <a href="https://harveynick.com/2016/06/06/material-design-and-ios/">I don’t think that’s necessarily a bad thing</a> on iOS, but it is worth baring in mind.</p>
<h2 id="summation">Summation</h2>
<p>I have to admit that I like developing with Flutter a great deal. Aside from a few frustrations it’s a genuinely great framework, though I wish its basis programming language was Swift, rather than Dart. I’m never completely happy with the app which results, however. I really, really wish Flutter produced native OS UI components, rather than its own OpenGL rendered widgets.</p>
<p>John Gruber’s <a href="https://daringfireball.net/2018/04/scuttlebutt_regarding_ui_project">little birds tell him</a> Apple’s rumoured cross platform UI framework<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup> is based on a declarative paradigm. If that turns out to essentially be Flutter, but written with Swift and producing native OS UI components I will be absolutely <em>thrilled</em>. That would be a serious sweet spot, in my opinion.</p>
<p>If I was going to make a recommendations to the reader, they would be:</p>
<ul>
<li>Consider using Flutter if you need to build a cross platform app, especially if you need to build for both iOS and Android in a hurry. Remember, though, that the differences between the platforms are more than fonts and scroll physics;</li>
<li>If you’re building just an Android app it might also be worth considering. Android developers tell me the Flutter APIs are a step up from the Android UI APIs, and Android users tell me Flutter based Android apps feel totally solid;</li>
<li>If you’re building for iOS: beware. Flutter apps don’t quite feel native to the platform. UIKit and something like <a href="https://github.com/ReactiveX/RxSwift">rxSwift</a> might be a better option;</li>
<li>If you’re really forward looking and want to build for <a href="https://en.m.wikipedia.org/wiki/Google_Fuchsia">Fuchsia</a> (or whatever it ends up being called), then yes: definitely use Flutter. I suspect that’s <em>very</em> forward looking, though.</li>
</ul>
<p><em>Edit (22/05/2018): Added a link to the Flutter for iOS Developers resource, removed the <code>new</code> keyword from the Dart code and removed the <em>e</em> from Michał because spelling his name is even harder than I thought.</em></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>S’up Michał, if you’re reading this. I hope you appreciate the ł. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Obviously I’m anthropomorphising here. Google Chrome is a piece of software. It does not make design decisions for itself. Yet. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>The biggest issue I’m aware of here is that certain mathematical operations are not guaranteed to be identical when transpiled to JavaScript. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>This isn’t a knock against either Android or Flutter itself. It stands to reason that a team within Google would have an easier time reproducing the Android look and feel than the iOS look and feel. Need to quantify the scroll physics used by Android? Just look at the code. That’s not really an option for iOS. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Cross platform in this case meaning iOS and macOS, plus potentially tvOS and watchOS. The Apple ecosystem only. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Fast.ai via iPad with Paperspace and Juno App]]></title><description><![CDATA[Getting started with the fast.ai "Deep Learning for Coders" MOOC. Setting Paperspace to work as a compute backend, and using the iPad app Juno as the frontend.]]></description><link>https://harveynick.com/2018/05/09/fast-ai-via-ipad-with-paperspace-and-juno-app/</link><guid isPermaLink="false">5aef7f21271a960022c0f4b2</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Wed, 09 May 2018 12:00:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Note: This is a repost from my <a href="https://ftrsn.net">other blog</a>.</p>
<p>Having started Fast.ai’s <a href="http://course.fast.ai">Practical Deep Learning for Coders</a> course, the first thing I noticed is how much less structured it is than Andrew Ng’s <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.278&amp;type=3&amp;subid=0">Coursera Deep Learning Specialization</a> (<a href="https://www.coursera.org/specializations/deep-learning">non affiliate link</a>).</p>
<p>Fast.ai supplies you with the <a href="http://jupyter.org">Jupyter</a> notebooks needed for the assignments, but here a lot of the setup is down to you. At first I was a little frustrated by the extra work that Fast.ai was making me do. Then I came to the conclusion that it’s actually a good thing. In the first instance, the less controlled environment is better preparation for actual problems.</p>
<p>In the second, it means I can try doing the whole course via iPad. I’ve already <a href="https://ftrsn.net/2018/03/17/some-notes-on-courseras-andrew-ng-deep-learning-speciality/">noted</a> that Jupyter in the browser is a pretty miserable experience on iPad. Happily there’s an excellent native Jupyter app called <a href="https://itunes.apple.com/gb/app/id1315744137?at=1010lLbq">Juno</a>, which solves that problem nicely. But a bit of extra work is needed to get it working well.</p>
<p>I decided to use <a href="https://www.paperspace.com/&amp;R=AAGWLUH">Paperspace</a><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> (Fast.ai’s recommended option) as my GPU cloud for this course. There are instructions for setting up Paperspace for fast.ai <a href="https://github.com/reshamas/fastai_deeplearn_part1/blob/master/tools/paperspace.md">here</a>. Once you’ve done that, your workflow will look something like this:</p>
<ol>
<li>Start your instance via the Paperspace console;</li>
<li>Log in via ssh and start Jupytor;</li>
<li>Copy the URL with the magic token;</li>
<li>Paste it into your browser, replacing <code>localhost</code> with your instance’s public IP;</li>
<li><em>Hack hack hack</em>;</li>
<li>Shut down your instance via the Paperspace console.</li>
</ol>
<p>Step 3 and 4 don’t work so well for Juno, and step 2 is also pretty superfluous. We can eliminate these by turning on password authentication and automatically starting Jupyter on boot.</p>
<p>Password authentication comes first, which will make connecting via Juno a lot easier. I’m assuming you’ve followed the setup I linked to above. Start your instance and log in via the terminal. Now run this on the commend line:</p>
<pre><code>cd fastai
jupyter notebook password
</code></pre>
<p>Then give it your chosen password. Next: run Jupyter on startup. Type this on the command line:</p>
<pre><code>crontab -e
</code></pre>
<p>Now add this to the bottom of the file which opens:</p>
<pre><code>@reboot cd /fastai; source /.bashrc; /anaconda3/envs/fastai/bin/jupyter notebook &gt;&gt;/cronrun.log 2&gt;&amp;1
</code></pre>
<p>Even though Jupyter will now start automatically, there are still reasons to log in. You’re going to need to download additional datasets, for one thing. <code>ssh</code> would be the usual means of doing so, but from the iPad <code>mosh</code> (short for <a href="https://mosh.org">“Mobile Shell”</a>) is a more robust option. I’m using an app called <a href="http://www.blink.sh">Blink</a> for that.</p>
<p>Paperspace machines are not set up to allow the ports mosh uses by default. So you’ll need to open one, like so:</p>
<pre><code>sudo ufw allow 60001
</code></pre>
<p>After that mosh should work just fine.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>That’s an affiliate link which will get you $10 of credit. If you prefer a non-affiliate link there’s one <a href="https://www.paperspace.com">here</a>. If you go that route and still want the $10 credit, you can use my code, which is: AAGWLUH. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Some Notes on Coursera’s Andrew Ng Deep Learning Speciality]]></title><description><![CDATA[Notes on the Coursera Speciality which acts as a follow up to the Machine Learning course I discussed previously.]]></description><link>https://harveynick.com/2018/05/07/some-notes-on-courseras-andrew-ng-deep-learning-speciality/</link><guid isPermaLink="false">5aef7a30271a960022c0f4ae</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Mon, 07 May 2018 12:00:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Note: This is a repost from my <a href="https://ftrsn.net">other blog</a>.</p>
<p>As with my <a href="https://ftrsn.net/2018/02/11/some-notes-on-the-andrew-ng-coursera-machine-learning-course/">previous post</a> on <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.248&amp;type=3&amp;subid=0">Coursera’s headline Machine Learning</a> course, this is a set of observations rather than an explicit “review”. There’s a heavy dose of “your mileage may vary” here. I’m aiming to lay out a set of objective observations about the course to help the reader decide if the course will be useful to them. That said: There will be opinions here.</p>
<p>I’ll be using that same ML course as a reference for comparisons. I’ll also make a comparison to the Udacity <a href="https://eu.udacity.com/course/intro-to-machine-learning--ud120">“Introduction to Machine Learning”</a> course I mentioned in the previous post. That’s a lot of “learning”, so I’ll be using the following acronyms to help maintain my sanity:</p>
<ul>
<li><strong>CML</strong> - <a href="https://en.m.wikipedia.org/wiki/Andrew_Ng">Andrew Ng</a>’s <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.248&amp;type=3&amp;subid=0">Coursera Machine Learning</a> course, originally taught at Stanford University;</li>
<li><strong>UIML</strong> - Sebastian Thrun and Katie Malone’s <a href="https://eu.udacity.com/course/intro-to-machine-learning--ud120">Udacity Introduction to Machine Learning course</a>;</li>
<li><strong>CDLS</strong> - The <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.278&amp;type=3&amp;subid=0">Coursera Deep Learning Speciality</a> by Andrew Ng’s <a href="https://www.deeplearning.ai">DeepLearning.ai</a>. i.e. The subject of this post.</li>
</ul>
<p>If at any point I’m talking about a course but haven’t specified which: assume it’s <strong>CDLS</strong>.</p>
<h2 id="cost">Cost</h2>
<p>Let’s get this out of the way first. Where as <strong>CML</strong> could be fully “audited” for free, <strong>CDLS</strong> cannot. To be clear: you can get just about everything out of <strong>CML</strong>, including grading of assignments, without having to pay a penny to Coursera. If, at the end of the course, you want a digital certificate: that will cost you £60. But if you don’t care about that, you don’t have to spend the money<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p>
<p><strong>CDLS</strong>, on the other hand, is subscription based. At the time of writing it costs £37 per month. You can watch at least some of the lectures without paying that, but you can’t do any of the coding assignments or access the course forums. The charge is fair enough, in my opinion. The content is new, and you’re learning from a master. This course requires more support resources than <strong>CML</strong>, as well. I’ll get to that in the “Coding Assignments” section. In fairness, it’s also a business, not a charity.</p>
<h2 id="coursecontent">Course Content</h2>
<p>The specialisation is actually made up of five separate courses. In order, these are:</p>
<ol>
<li><em>Neural Networks and Deep Learning</em> (4 weeks);</li>
<li><em>Improving Deep Neural Networks: Hyperparameter Tuning, Regularisation and Optimisation</em> (3 weeks);</li>
<li><em>Structuring Deep Learning Projects</em> (2 weeks);</li>
<li><em>Convolutional Neural Networks</em>, aka image processing (4 weeks);</li>
<li><em>Sequence Models</em>, aka language processing (3 weeks).</li>
</ol>
<p>You can choose to take them in any order, or to skip any you’re not interested in. Each builds on those before<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>, though, so my advice would be to take each course in the specified order. The length of each course in weeks is really just a guidance figure. Each “week” is actually a little under 2 hours of video lectures, plus graded assignments. You can take it as quickly or slowly as you like<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>The first three weeks of the first course overlap quite heavily with the parts of <strong>CML</strong> which teach neutral networks. The third course also has some crossover with the sixth (<em>Advice for Applying Machine Learning</em> and <em>Machine Learning System Design</em>) and tenth (<em>Large Scale Machine Learning</em>) weeks of <strong>CML</strong>. The material has been updated, however, and made more applicable to deep neural networks.</p>
<h2 id="teachingmethod">Teaching Method</h2>
<p>The teaching methodology is basically identical to <strong>CML</strong>. Ng talks to the camera, or he talks whilst annotating slides. It is, again, a pretty direct conversion from an in person classroom lecture to video format. There are a couple of exceptions to this in the main lectures, which show Ng interacting with an implemented system.</p>
<p>The other exception to this format are the optional “heroes of deep learning” interviews which are included at the end of five of the lectures in the first two courses.  The subjects of the interviews are: Geoffrey Hinton, Pieter Abbeel, Ian Goodfellow, Yoshua Bengio, and Yuanqing Lin. In my opinion the Hinton interview is the one most worth your time.</p>
<p>Again as with <strong>CML</strong>, each week’s lecture is broken down into more focussed individual videos of between 5 to 15 minutes.</p>
<p>I have to mention the first quality issue here. In some places it’s actually really badly edited. Ng sometimes makes false starts and begins again. At first I though the video was skipping, but then I noticed small changes in what Ng said at the times he repeated himself. This happens about once a video, on average. There are also occasional long pauses in the dialog, suggesting that Ng has lost his place in his notes. I find it pretty baffling that these glitches haven’t been edited out. Hopefully it’ll get fixed in an update to the course at some point.</p>
<p>Alongside the videos are the graded assignments. Every week there is a quiz, usually with 10 to 15 questions. A score of 80% or higher is required to pass, but you’re allowed to retake the quiz if you initially get some of the questions wrong. Most weeks also have coding assignments. This isn’t true of the very first week, which serves as an introduction. Nor is it true of the third course, <em>Structuring Deep Learning Projects</em>. This is assessed with longer quizzes, which it calls “machine learning flight simulators”.</p>
<p>Editing issues aside, the teaching worked really well for me. In particular, I feel like I came out of the sequence models course with a level of understanding I’ve failed to get from other sources.</p>
<h2 id="codingassignments">Coding Assignments</h2>
<p>The coding assignments are something I feel <strong>CDLS</strong> really gets right. To recap, both <strong>CML</strong> and <strong>UIML</strong> have you download datasets and outline code. You modify the code locally, run it to make sure it works, and then either submit it for online assessment or answer questions about its output. <strong>CML</strong> has you code (almost) everything from scratch using <a href="https://www.mathworks.com/products/matlab.html">Matlab</a>/<a href="https://www.gnu.org/software/octave/">Octave</a>. For <strong>UIML</strong> you use <a href="https://www.python.org">Python</a>, and mostly parameterise library implementations of the relevant algorithms from <a href="http://scikit-learn.org/stable/">SciKit Learn.</a></p>
<p><strong>CDLS</strong> uses the best parts of both of these approaches, in my opinion. The teaching language is Python. You begin in the first course by coding neural networks and optimisation algorithms completely from scratch. This is done using <a href="http://www.numpy.org">NumPy</a>, which essentially adds most of the numerical computing features of Matlab/Octave to Python. The second course then introduces <a href="https://www.tensorflow.org">TensorFlow</a>, a much higher level framework which does a lot of the work for you. Later in the fourth course, <a href="https://keras.io">Keras</a> (an even higher level framework) is introduced and used. Still, even during the later assignments you might occasionally use NumPy when it makes sense to teach you about an algorithm or technique.</p>
<p>Rather than having you download the code and run it locally, <strong>CDLS</strong> instead uses <a href="http://jupyter.org">Jupyter Notebooks</a> hosted by Coursera. This is a web based IDE, which allows you to code in your browser. It mixes code, blocks of descriptive text, formulas, and images. This makes it an excellent teaching tool. Being browser based, you can also access it from anywhere without needing to download anything<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>. I actually did one of the assignments from my iPad when I couldn’t use my laptop<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>.</p>
<p>Here, unfortunately, I also need to mention another quality issue. Two of the assignments in the last course had incorrect “expected output” values. The upshot of which was that I spent over an hour in total trying to figure out how to “fix” my code, when it was actually working perfectly. In both cases I eventually discovered “errata” forum posts detailing the issue. I guess the lesson here is: always check the forums first. Again: it’s pretty frustrating that this hasn’t been fixed in the notebook and I needed to refer to the forum at all.</p>
<p>It’s worth noting here that server time isn’t free. Running the Jupytor notebooks for the coding assignments is one of the things your £37 a month<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup> is paying for.</p>
<p>I do have a small complaint about the specifics of that server time, though: it’s running on CPU instances. This means that training times are much longer than they would be on a GPU instance. In the earlier courses this means you might need to occupy yourself for 15 minutes whilst your network trains. In the later courses training just becomes infeasible and you work with pre-trained models instead. I can only assume that using GPU instances would make the course prohibitively expensive, either for the user or Coursera themselves.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Going into this course, you should already have some experience of coding in Python. Fortunately Python is an easy language to pick up. <a href="https://www.hackerrank.com/">HackerRank</a> is my favourite “coding dojo” for when I need to skill up on a new programming language. I think their “30 days of code” challenge is probably a good place to start for someone new to coding.</p>
<p>Additionally, I would recommend doing the <strong>CML</strong> course first. It’s not all directly relevant, but will give you some good intuition and background. For example: when Ng using the phrase “large margin classifier” in CDLS, you’ll know exactly what he’s talking about right away.</p>
<h2 id="whatsnext">What’s Next</h2>
<p>Broadly I’m sticking to the plan I wrote at the end of my write up of <strong>CML</strong>. Next: Fast.ai’s <a href="http://course.fast.ai">Practical Deep Learning for Coders</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>For the record, I did spend the money. In the first instance: curiosity about what the certificate would actually be. In the second: I thought the course was great and felt Coursera had earned the money. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>To a greater or lesser extent. The earlier courses are quite foundational, the latter ones less so. That said: you will miss some nuiance in the Sequence Models course if you skip Convolutional Neural Networks, for example. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Though if you take it too slowly the system will by default start to bug you with notifications and emails. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Aside from the content of the web page. Obviously. You know what I mean. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>I don’t recommend that, though. Jupytor breaks the inertial scrolling in mobile safari, which can make it pretty frustrating to use. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>Or local equivalent. <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[I Ain’t Dead]]></title><description><![CDATA[In which I make the case that I haven’t entirely been sitting on my hands when it comes to writing for the web. Also: pontifications on the differences between Ghost and Wordpress.]]></description><link>https://harveynick.com/2018/05/06/i-aint-dead/</link><guid isPermaLink="false">5aef76e0271a960022c0f4ab</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 06 May 2018 21:45:20 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>I know, it’s been pretty damn quiet around here. Aside of course, from a single <a href="https://harveynick.com/2018/04/25/some-notes-on-the-andrew-ng-coursera-machine-learning-course/">repost</a> from my other blog. But I’d like to cite two pieces of evidence to prove that I haven’t been doing nothing at all:</p>
<ol>
<li>This site has a shiny new theme;</li>
<li>I’ve actually been posting multiple times a week over at my <a href="https://ftrsn.net">Future Technology</a> blog.</li>
</ol>
<p>Lets talk about the theme first. It’s called <a href="https://cedar-ghost.ecko.me">Cedar</a>, and I paid actual money for it over at <a href="https://themeforest.net/item/cedar-responsive-ghost-theme/13127587?s_rank=6">Theme Forest</a>. Why pay money for a theme, when there are plenty of free ones available? I didn’t like any of the free ones enough, essentially. I tend to believe that the intersection of talent and compensation leads to high quality work. I was pretty happy to pay a fair price for a good design<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p>
<p>I know just enough web development to be dangerous and I wrote this site’s previous theme. It was… functional, but I was never completely happy with the design. It also had a bunch of odd CSS issues, and I didn’t relish the thought of updating it if a new major version Ghost brakes compatibility.</p>
<p>As suggested above, this site still runs on <a href="https://ghost.org">Ghost</a>, whereas <a href="https://ftrsn.net">ftrsn.net</a> is <a href="https://wordpress.org">Wordpress</a> based. Having used it pretty solidly for a few months, I’d like to talk a little about the things I like about it. Then I’d like to talk about why I have no plans to move this site over to it, and why a future project I’m working on will likely also use Ghost.</p>
<p>One point in Wordpress’ favour is that it’s ubiquitous. What <a href="https://harveynick.com/2017/11/06/blogging-on-the-quartz-curve/">I said before</a> is still true now. Getting up and running with Wordpress can be both free and easy<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.  This ubiquity means that there are plenty of themes available for it, and plugins to do just about anything you might want. I try to run as few plugins as possible on the site, on the basis that any of them could have a security flaw. But still, I currently have eight active plugins:</p>
<ol>
<li><strong>Disable Comments</strong>. I don’t use them on the site, this makes sure there isn’t even the option to accidentally enable them;</li>
<li><strong>Jetpack by Wordpress</strong>. To add all of the missing features which otherwise would require hosting at <a href="https://wordpress.com">Wordpress.com</a>;</li>
<li><strong>Google Analytics</strong>. To fill in the gaps Jetpack misses, although I am looking at alternatives;</li>
<li><strong>Limit Login Attempts</strong>. For security;</li>
<li><strong>miniOrange</strong>. Adds 2 factor authentication, again for security;</li>
<li><strong>Really Simple SSL</strong>. To force https to be used instead of http when possible;</li>
<li><strong>WP Super Cache</strong>. To try and pretend that Wordpress isn’t painfully slow;</li>
<li><strong>WP to Twitter</strong>. Handles automatic posts to Twitter <em>much</em> better than Jetpack.</li>
</ol>
<p>I also have 2 deactivated plugins which I keep around in order to workaround a few missing capabilities of Wordpress:</p>
<ol>
<li><strong>Categories to Tags</strong>. Because otherwise I’d need to use database queries(!) to do this;</li>
<li><strong>Post Type Switcher</strong>. As above.</li>
</ol>
<p>As for themes: Wordpress makes it really easy. If the theme is available on Wordpress’ marketplace, you can install and activate it from the Wordpress console. Any configuration parameters can then be edited from the console as well, with the live UI to show you the changes. You can also also edit the theme files directly, again from the Wordpress console. Don’t do that, though. In the first instance it might break. In the second: when you use Wordpress, your website is a sausage. You do <em>not</em> want to see how it gets made. In short, though: You can edit your site <em>from</em> your site.</p>
<p>One thing which I have nothing but good things to say about is the workflow I’ve been able to achieve with Wordpress. <a href="https://ulyssesapp.com">Ulysses</a>, my editor of choice, has Wordpress export built in. I can seamlessly move between writing on my laptop and my iPad<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. The iPad, when coupled with a smart keyboard, turns out to be an absolutely spectacular machine for this. It’s light and small enough that I barely even notice if it’s in my bag. It easily lasts a week between charges with my usage. I was also able to build <a href="https://workflow.is">workflows</a> which make the process of creating posts in the format I use for <a href="https://ftrsn.net">ftrsn.net</a> incredibly easy. Once written, posts take two taps to publish.</p>
<p>Now it comes to what I <em>don’t</em> like about Wordpress. It essentially boils down to this: it’s gross. The Wordpress console might be powerful, but I never look forward to using it. It feels like a Frankenstein app, hacked together out of miscellaneous spare parts. When you activate a plugin, it’s free to do just about anything it likes. I’ve found at least three different places in the console UI where plugins might install their settings pane. It’s clunky, and slow. The version of the Wordpress console used on Wordpress.com is quite clean and pleasantly designed, but clearly <a href="https://automattic.com">Automattic</a> are keeping that for themselves. But even then, I know it’s php under the covers.</p>
<p>But what of Ghost? I did say I was sticking with it and planning to use it again in the future.</p>
<p>Ghost tends to be more expensive to host (especially if you use Ghost’s own plans, which are now aimed a professional publishers<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>). As for ease of setup, the new <a href="https://docs.ghost.org/docs/ghost-cli">Ghost CLI</a> has actually made that pretty easy. With it in place, I think I’d actually be more comfortable self hosting Ghost than I would Wordpress.</p>
<p>At the time of writing there is no such thing as a Ghost plugin. There is a thing called an <a href="https://apps.ghost.org">“app”</a>, but thus far only first party apps are available. Happily with Ghost I don’t really need much in the way of plugins at the moment. What I do need can be accomplished via code injection, <a href="https://ifttt.com">IFTTT</a>, or with the first party apps which are available.</p>
<p>Ghost also has no UI for editing themes. Themes are uploaded as .zip files, and applied to your site as is. This means that to configure Cedar to work the way I want I had to edit 1 Javascript file, and several <a href="https://handlebarsjs.com">handlebars</a> files directly. Nothing too onerous, but still more hassle than the Wordpress equivalent.</p>
<p>The workflow for uploading to Ghost isn’t as nice. I have to copy and paste into the web UI, rather than just clicking two buttons in Ulysses. But that’s fine for a blog I update every <em>cough</em> few weeks <em>cough</em> (as opposed to one I update a few times a week).</p>
<p>Despite all of the above, I really like Ghost. Workflow issues aside, it’s a pleasure to use. It’s fast and well designed. If I need to edit the theme of my site I’m not horrified by what I find there.</p>
<p>Going forward a lot of the long posts I had planned for <a href="https://ftrsn.net">ftrsn.net</a> are going to be cross posted here, or posted here and linked to from there. They’re about my journey in trying to skill up on Machine Learning, and that feels like more of a good fit for this site.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>It actually shook out to just under £20 once VAT and credit card fees were applied. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Although there are options which are neither, if that’s what you’re looking for. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Or even my iPhone, if I need to make a quick edit on the go. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Thankfully they’re keeping me on a “legacy” plan, which is only costing me a few pounds a month. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Some Notes on the “Andrew Ng” Coursera Machine Learning Course]]></title><description><![CDATA[Some notes on the MOOC which is more or less the standard text for basic machine learning. Comparisons are made with Udacity's Introduction to Machine Learning.]]></description><link>https://harveynick.com/2018/04/25/some-notes-on-the-andrew-ng-coursera-machine-learning-course/</link><guid isPermaLink="false">5ae03bb14304db00222e74d4</guid><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Wed, 25 Apr 2018 08:30:13 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Note: This is a repost from my <a href="https://ftrsn.net">other blog</a>.</p>
<p>I was originally going to write this as a “review”, but this course is now considered such a foundational resource that writing a review would feel presumptuous and redundant. Then I was going to write it as a list of pros and cons, but I came to the conclusion that this would probably be subjective. So instead I’m writing a set of notes to be interpreted by the reader.</p>
<p>I originally started the <a href="https://eu.udacity.com">Udacity</a> <a href="https://eu.udacity.com/course/intro-to-machine-learning--ud120">Introduction to Machine Learning</a> course in preference to <a href="http://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.4&amp;type=4&amp;subid=0">Coursera’s</a> Stanford University <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.248&amp;type=3&amp;subid=0">Machine Learning</a> course<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, for reasons which I’ll come to. As for why I switched, I’ll come to that as well. I’ll use the Udacity course as a point of comparison throughout. Please note, though, that I’ve only followed the first 4 weeks of the Udacity course.</p>
<h2 id="teachers">Teachers</h2>
<p>The Coursera course is taught by <a href="https://en.m.wikipedia.org/wiki/Andrew_Ng">Andrew Ng</a>, Professor at Stanford University, former chief scientist at <a href="https://en.m.wikipedia.org/wiki/Baidu">Baidu</a> and co-founder of the following things:</p>
<ul>
<li><a href="https://research.google.com/teams/brain/">Google Brain</a>;</li>
<li><a href="https://www.deeplearning.ai">deeplearning.ai</a>;</li>
<li><a href="https://www.coursera.org">Coursera</a> itself.</li>
</ul>
<p>Needless to say he, he knows his stuff. He also delivers it in a very direct, understandable and sometimes self affacing manner.</p>
<p>The Udacity Course is taught by <a href="https://en.m.wikipedia.org/wiki/Sebastian_Thrun">Sebastian Thrun</a> and Katie Malone. Sebastian Thrun is also a Professor at Stanford University, as well as Georgia Tech. He led the team which won the <a href="https://en.m.wikipedia.org/wiki/DARPA_Grand_Challenge">DARPA Grand Challenge</a> in 2005. He also co-founded:</p>
<ul>
<li><a href="https://x.company">Google X</a>;</li>
<li>The self driving car project within Google X which became <a href="https://waymo.com">Waymo</a>;</li>
<li><a href="https://www.udacity.com">Udacity</a> itself.</li>
</ul>
<p>Katie Malone is currently the Director of Data Science Research and Development at Civis Analytics. A Stanford PhD, she was an intern at Udacity when the Intro to ML course was made (I think). She handles around 70% of the teaching in the course.</p>
<p>At the time I started the course, I had not heard of Andrew Ng, but was very aware of Sebastian Thrun. So that was (at the time) a point in favour of the Udacity course.</p>
<p>Looking back now that I know more about Ng, I’d say it’s quite hard to pick who has the more impressive CV. It feels<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> as though Andrew Ng is a little more respected in the machine learning field, however.</p>
<h2 id="teachingmethod">Teaching Method</h2>
<p>The teaching method of the Coursera course is a fairly direct conversion of a standard in-person lecture. Ng talks directly to the camera, or talks while digitally annotating his lecture slides.</p>
<p>The Udacity course plays with the format much more. Having two course leads means that there can be dialogue between them. Sections of some lectures are delivered from inside a self driving car. The leads also joke with each other at times. Making fun of each other’s taste in music, for example, as part of the explanation of a music recommendation system.</p>
<p>Your mileage may vary regarding which of these teaching methods works best for you. I found those used in the Udacity course to be more engaging. It felt as though it used more of the potential of an online course than the approach  the Coursera course takes. At no point in the Coursera course does Andrew Ng sing “Let It Go”. I’ll let you decide whether this is a positive or a negative.</p>
<p>That said, I think I retained more of the knowledge from the Coursera course, so perhaps its explanations were clearer. I suspect it also has a lot to do with the next point.</p>
<h2 id="codingassignments">Coding Assignments</h2>
<p>The Udacity course uses <a href="https://www.python.org">Python</a> as its teaching language. The Coursera course uses <a href="https://www.mathworks.com/products/matlab.html">Matlab</a>/<a href="https://www.gnu.org/software/octave/">Octave</a>.</p>
<p>Superficially, this is a huge point in the favour of the Udacity course. Python is essentially <em>the</em> language of machine learning at this point. It also has a much bigger ecosystem surrounding it<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>For me, at the time, that was enough to make me choose to move forward with the Udacity course.</p>
<p>When I revisited the Coursera course I realised that there’s another significant difference. The Udacity course imports its implementations of the various algorithms from <a href="http://scikit-learn.org/stable/">SciKit Learn</a>. Most of your work in the first three assignments is to initialise the correct class from the library, set it training on the data, and wait.</p>
<p>For the Coursera course, on the other hand, you implement almost everything from scratch. As a result, you learn more about how the algorithms actually work.</p>
<p>In real world use the first methodology makes a lot of sense. There’s no need to reinvent the wheel. For educational purposes I think it’s preferable to learn the lower level nuts and bolts of the algorithms. Even if the choice of language and programming environment is somewhat... suboptimal, in my opinion<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>.</p>
<p>One further difference: the datasets are much larger in the Udacity assignments. Depending on how powerful the machine you’re using is, it might take 15 minutes or so to finish training the models for the assigments. You’ll also need to download a roughly 4GB zip with the data before starting the first assignment.</p>
<h2 id="coursecontent">Course Content</h2>
<p>The Udacity course is 10 weeks long, where as the Coursera course is 16 weeks. That being the case, clearly the latter has more than 50% more room for content. Even so, the Udacity course teaches several shallow learning methods<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup> which are not present in the Coursera course. The breadth of a Coursera course is much larger, however, and it’s the only one of the two which covers neural networks<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>.</p>
<p>You can read the syllabus of both courses before enrolling, so it’s easy to see whether a particular technology of interest is present.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>In both cases, I would say that some programming experience is needed. Both Python and Matlab/Octave are reasonably easy to pick up, though. Based on the assignments I finished, the Udacity course requires the least actual programming.</p>
<p>Please note: If your machine learning needs are limited to training and deploying existing models, you might only need to learn a bare minimum of coding in order to do so.</p>
<h2 id="whatsnext">What’s Next?</h2>
<p>This course was the first part of a syllabus I built for myself when I started trying to skill up on machine learning. It wasn’t part of my original plan, but Andrew Ng released his new <a href="https://click.linksynergy.com/fs-bin/click?id=X5M728uyIs8&amp;offerid=467035.278&amp;type=3&amp;subid=0">Deep Learning Specialisation</a> on Coursera just as I was finishing the last few weeks of his Machine Learning course. That seemed somewhat serendipitous, so that’s what I’m working on now. I’ll write a similar set of notes on that course after I’ve finished.</p>
<p>After that I plan to follow both of <a href="https://harveynick.com/2018/04/25/some-notes-on-the-andrew-ng-coursera-machine-learning-course/www.fast.ai">fast.ai</a>’s deep learning courses. And after that I’m planning on following the <a href="https://www.thegreatcourses.com/courses/philosophy-of-mind-brains-consciousness-and-thinking-machines.html">Philosophy of Mind</a> Series from the <a href="https://www.thegreatcourses.com">Great Courses</a>. I might also finish up the Udacity Machine Learning course to help fill in the gaps in my shallow learning knowledge. That said: No battle plan survives contact with the enemy<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup>, so I guess I’ll see.</p>
<p>On an entirely different strand of learning, I’m also following the Princeton University <a href="https://www.coursera.org/learn/cryptocurrency">Bitcoin and Cryptocurrency</a> course on Coursera.</p>
<p><a href="https://news.ycombinator.com/item?id=16353829">Discuss this post on Hacker News.</a></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Which tends to get referred to as “The Andrew Ng Machine Learning Course”, hence the title of this piece. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>To me, at least. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>It also numbers its arrays from zero, just like God intended. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Matlab/Octave is not an environment you’re likely to use in production. It also numbers arrays from 1, where most other programming languages number from 0 (as I noted before). This difference can definitely lead to bugs if you’re not careful. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Such as <a href="https://en.m.wikipedia.org/wiki/Naive_Bayes_classifier">Naive Bayes</a> and <a href="https://en.m.wikipedia.org/wiki/Decision_tree">Decision Trees</a>. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>Which is probably what you’re interested in if you’re starting a machine learning education today. <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn7" class="footnote-item"><p>Or “everyone has a plan until they get punched in the face”, if you prefer Mike Tyson’s trainer’s version. <a href="#fnref7" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[International Men’s Day]]></title><description><![CDATA[<div class="kg-card-markdown"><p>As promised back in <a href="http://harveynick.com/2017/03/08/international-womens-day/">my International Women’s Day post</a>, I’m now writing about its counterpart. International Men’s Day is the day I’m posting this: the 19th of November. As noted in the previous post: some people would say that we don’t need this day, because</p></div>]]></description><link>https://harveynick.com/2017/11/19/international-mens-day/</link><guid isPermaLink="false">5a11c4b67cf56000218249ac</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 19 Nov 2017 17:59:56 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>As promised back in <a href="http://harveynick.com/2017/03/08/international-womens-day/">my International Women’s Day post</a>, I’m now writing about its counterpart. International Men’s Day is the day I’m posting this: the 19th of November. As noted in the previous post: some people would say that we don’t need this day, because the other 364 days of the year already serve that purpose. I say we do need it. In the first instance because that’s not the way it should be.</p>
<p>In the second instance I say we need it because the fact of the matter it is: Men have problems too. I hope we can accept that without feeling like it devalues women’s problems<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. It would be laughable to claim that men’s problems approach the number or severity of those which women face. So please don’t think I’m doing that. But we, men, do nevertheless have problems. Beyond that fact, many of the problems women face boil down to one thing: men. Us. That too is our problem.</p>
<p>I’m going to be talking directly <em>to</em> other men in this post. If that’s not you, that doesn’t necessarily mean that it doesn’t apply to you. Especially if there are men in your life whom you care about. Conversely, I don’t claim that I’m speaking <em>for</em> all men. Just that I’m speaking <em>as</em> one.</p>
<p>The evidence would suggest that men (as a population) don’t do right by women (as a population). So as not to belabour the point that I’m not talking about individuals: you can assume from this point on, that whenever I say “men” or “women” I’m talking about populations. The thing is: men don’t do right by men, either. We need to work on both. Perhaps making progress in the latter will help us with the progress we need to make in the former.</p>
<p>When I wrote the IWD post, I barely even had to think about which of women who’ve inspired me I’d talk about. It was obvious. As it turns out, I find it much harder to think of inspirational men to talk about on the same terms. I don’t want to underplay the role my father and both grandfathers have had in my life, because truly I would not be the person I am today without their support, their love, and most of all their example. Beyond them, though, it’s hard to think of anyone. There are historical figures I find inspirational. Men I’ve never met, such as Mahatma Gandhi, Alan Turning and Douglas Adams. But as I say: I’ve never met them. Their influence on me is indirect and vicarious.</p>
<p>Which raises the question: Where should we look for male role models in the here and now? Or if not the here, at least in the now? I’ll come back to this.</p>
<p>In the meantime, though. Let’s talk about Chester Bennington. The former lead singer of the rock band Linkin Park, who committed suicide earlier this year. You might not be a fan of his music. People certainly seemed to feel the need to tweet about it if they weren’t on the day he died. For me, he was one of my favourite singers. I have a real weakness for people who sing like it’s the only way to scare the demons away<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. The problem, though, is that sometimes the demons catch up.</p>
<p>Along with two of his band mates (and Dr Ken), Chester recorded an episode of “Carpool Karaoke” days before he died. His family asked that it be released. You should <a href="http://www.nme.com/news/music/linkin-park-chester-bennington-carpool-karaoke-2147968">watch it</a>. Not because it’s especially good, because in my opinion it’s not. It’s awkward and weird. The music and singing really aren’t that great. You should watch it because it’s a video of a man recorded days before he would take his own life. It’s there. In that video. Somewhere inside him. The thing which would kill him. And it’s almost impossible to see.</p>
<p>Even if you don’t care one iota about Chester Bennington, we need to talk about Chris Cornell. Robin Williams. The list goes on. I could list more but I just don’t want to.</p>
<p>Perhaps you know that rates of suicide are <a href="https://en.m.wikipedia.org/wiki/Gender_differences_in_suicide">much higher for men than for women</a>. At least, I hope you do. What you might not know, though, is that the rate of suicide attempts is much closer to even, even swinging the other way. The main reason for this, it seems, is the choice of method. Men chose methods which end their lives there and then. Women choose methods which take time and allow them to be saved, either by themselves or by others. Women’s suicide attempts tend to be of a kind which gets labelled as a “cry for help”.</p>
<p>Look at that. Turn your head to the side. Squint your eyes. Eventually it starts to look like what’s happening here is that men don’t cry for help. We don’t even ask for it. We should start.</p>
<p>So, let’s loop back around to the question of role models. The short answer is: I can’t tell you who your role models should be. But I can perhaps suggest some places to look. Tim Ferriss has spoken openly about <a href="https://youtu.be/5J6jAC6XxAI">his own battle with depression and suicide</a>. My first suggestion of a place to look is in the guests of his podcast. Even if you don’t like Ferriss himself, his list of interviewees is phenomenal. Start, I’d say, with <a href="https://tim.blog/2016/05/22/sebastian-junger/">Sebastian Junger</a> and perhaps his book <a href="http://www.amazon.com/dp/1455566381/?tag=ftrsnnet-20">Tribe</a>.</p>
<p>Masculinity gets a bad rap. It’s easy (and sometimes apt) to attach the word “toxic” as prefix. After a while you can’t think the first without at least part of your mind adding the second. But for men, masculinity is basically unavoidable. Toxic masculinity is bad. But so, I think, is toxic <em>lack</em> of masculinity. The hard part is figuring out the right amount and, more importantly, the right kind. The hard part is figuring out what exactly that looks like. I’m honestly not certain that I can even give a satisfying <em>definition</em> for the word.</p>
<p>Another place to look is <a href="https://www.artofmanliness.com">The Art of Manliness</a> blog and podcast. It’s dedicated, more or less, to figuring that out.</p>
<p>That’s all I have for now really. I don’t think I’ve even scratched the surface of the issues here<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. I'm sorry it's so disjointed. Happy International Men’s Day, everyone.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>If you don’t think that’s the case then I humbly submit that you might be part of the problem. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>See also: Lacey Sturm. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Maybe next year we should talk about violence. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Blogging on the Quartz Curve]]></title><description><![CDATA[<div class="kg-card-markdown"><p>A little while ago, over dinner, a good friend of mine introduced me to something called the “Quartz Curve”. Named for the online news magazine which coined it, it goes like this: if you plot the length of an article against user engagement, the resulting graph is bowl shaped. Specifically,</p></div>]]></description><link>https://harveynick.com/2017/11/06/blogging-on-the-quartz-curve/</link><guid isPermaLink="false">5a00304d26662600222adc3a</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Mon, 06 Nov 2017 12:00:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>A little while ago, over dinner, a good friend of mine introduced me to something called the “Quartz Curve”. Named for the online news magazine which coined it, it goes like this: if you plot the length of an article against user engagement, the resulting graph is bowl shaped. Specifically, the trough is between 500 and 700 words.</p>
<p>Shorter articles can be “Short, sharp creative takes on news stories that are creative and say something new”. Conversely, longer articles can provide “strong detailed narrative or insightful analysis”. Anything in between, though, will tend to be wishywashy, and no one wants that.</p>
<p>In the words of Quartz editor-in-chief <a href="https://www.themediabriefing.com/article/is-this-article-too-long-why-newspapers-are-getting-their-article-length-just-wrong-for-the-web">Kevin Delany</a>:</p>
<blockquote>
<p>The place between 500 and 800 words is the place you don't want to be because it's not short and fast and focused and shareable, but it's not long enough to be a real pay-off for readers.</p>
<p>The standard of production for most traditional news organisations is still somewhere within that range. For a digitally native organisation there's an opportunity.</p>
</blockquote>
<p>This got me thinking about this blog, the kind of articles I write here, and other blogs which I find to be worthwhile. Generally speaking, I aim for about 1000 words per article, which puts me (just) north of the Quartz Curve danger zone. I’ve been aiming for about one article every two weeks this year, but my average has been closer to one every three. Another point is that this blog has no real focus, aside from things I’m currently interested in.</p>
<p>When I think of other blogs (rather than bonafide news sites) which I consider really worthwhile, they do tend to mostly line up with the Quartz Curve. In terms of blogs which feature long, insightful (but somewhat infrequent) posts, two good examples are <a href="https://stratechery.com"> Ben Thompson’s Stratechery</a><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> and <a href="http://unenumerated.blogspot.co.uk">Nick Szabo’s Unenumerated</a>. Both sit at the intersection of technology and law. The first has a roughly weekly cadence and long but not unwieldy posts. The later has posts as and when the author feels like it, with months going by between posts. The resulting posts are... comprehensive. It would be fair to say that Szabo only speaks when he has something to say.</p>
<p>Stratechery has a quite a strong brand. It sits at it’s own domain. It also has a cohesive design, down to the “hand drawn”<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> style of the diagrams Thompson uses. Unenumerated, on the other hand, sits at a vanilla blogspot sub-domain. It looks to to use the default blogger template. Lastly, Szaro’s diagrams and images tend towards the... functional. So there are definitely similarities and differences between the two.</p>
<p>Another example of note which straddles both walls of the Quartz Curve is <a href="https://daringfireball.net">John Gruber’s Daring Fireball</a>. It’s probably the most notable and popular example of the “linklog” blog format<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. Most posts are short commentary on external sources. The title of the post actually links to that original sources and the same is true of the entries in the RSS feed (<a href="https://daringfireball.net/linked/2017/11/02/apple-q4-results">example</a>). These posts are very frequent (usually Gruber posts multiple times a day), and range across the author’s interests<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>. I often disagree with Gruber (he really can’t see straight when it comes to Google and other companies which compete with Apple), but he can also be very insightful. Particularly in his less frequent longer posts, such as <a href="https://daringfireball.net/2004/04/spray_on_usability">this fantastic example</a> from way back in 2004. That said, even at his most insightful, Gruber remains highly opinionated and lacks the critical detachment of Thompson or Szaro.</p>
<p>Which brings me back around to this blog and my own writing. I do plan to keep writing here. I’m also pretty keen on trying something a little more focussed. I want to try mixing together the short commentary with longer, more in depth and thought out writing. To that end I’ve started working on two other blogging projects.</p>
<p>The first I’ve launched already. It’s a blog about future technology called “Future Soon”<sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>. You can find it at <a href="https://harveynick.com/2017/11/06/blogging-on-the-quartz-curve/ftrsn.net">ftrsn.net</a>. Generally, it follows the “linklog” format. Originally I followed it exactly, but found some issues with that, so instead each post now begins with a “Source” attribution. I’m planning on posting these short referential posts with something like a daily cadence, and then writing occasional longer posts when I feel like I can add something. So Gruber’s format, with longer articles using Szabo’s cadence and Thompson’s style. Probably.</p>
<p>As to what issues I had with the linklog format... well, first I’ll have to tell you why I’m using Wordpress, despite previously expressing my distaste for it. It comes down to two things: price and ubiquity. I like Ghost (which this blog uses) a lot, but there’s no getting around the fact that it is more expensive. Compare the cost of <a href="https://ghost.org/pricing/">hosting Ghost with it’s makers</a> vs the <a href="https://wordpress.com/pricing/">equivalent options at Wordpress.com</a>. For this experiment I actually went even cheaper than that. I’m paying about £1.25 a month for Wordpress hosting from <a href="https://my.tsohost.com/aff.php?aff=5015">TsoHost</a><sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>. This is much easier for me to justify to myself.</p>
<p>I could also self host Ghost, which is an option TsoHost offers for an extra £3 a month. Which is where the next issue with Ghost shows up: it’s lack of ubiquity. I trust Ghost themselves to support a Ghost blog. They’re the experts. I have less faith in a third party, and even less faith in a second party (meaning me).</p>
<p>I’m also planning to run <a href="https://harveynick.com/2017/11/06/blogging-on-the-quartz-curve/ftrsn.net">ftrsn.net</a> almost entirely from my phone and iPad. Wordpress has a really solid iOS app, plus good integration with both Ulysses and IA Writer. Running a Ghost blog entirely through the web interface on a smaller device creates far too much friction.</p>
<p>As I said before, I originally followed the linklog format exactly. This meant installing a plug-in and editing the theme to work correctly with it. It all worked fine, but it made me nervous. One of the main things I dislike about Wordpress is the hot mess which is it’s plug-in and theming system. One incompatible update could break everything. As it stands I’m using a slightly customised version of an existing theme, and very few plug-ins, which feels much safer. Should the blog go well enough that I don’t mind paying a bit more per month, I can easily transfer it to the safety of Wordpress later.</p>
<p>Likewise if using Ghost from an iOS device becomes a better experience I'd be tempted to pay the extra for Ghost hosting. It really is that much more pleasant.</p>
<p>Now... I did mention a second other blog project. That’s going to be a travel site, of sorts. More about it when it’s ready. Which it isn’t.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>That’s <em>stra-tech-ary</em>, not <em>strate-cherry</em>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>I’m guessing with an Apple Pencil. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Nick Heer’s <a href="https://harveynick.com/2017/11/06/blogging-on-the-quartz-curve/pxlnv.com">Pixel Envy</a> is another good example. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>Chiefly Apple related tech news, but also baseball, plus Kubrick and James Bond movies, among other things. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>Yes, it is named after a <a href="https://youtu.be/1BmK8PYm504">Jonathan Coulton song</a>. <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>If you click through here, you can use the code “HarveyNick” for a 10% discount on hosting, which will kick a small amount back to me. <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Mostly an iPad Followup]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Somewhat appropriately, I finished and edited my <a href="http://harveynick.com/2017/07/30/thinking-about-going-ipad-mostly/">previous post about switching to an iPad for many of my computing use cases</a> <em>on</em> an iPad.  Luckily I have access to one which belongs to my employer and happens to be running the <a href="https://www.apple.com/ios/ios-11-preview/">iOS 11 Beta</a>. Since I was traveling at the</p></div>]]></description><link>https://harveynick.com/2017/08/13/mostly-an-ipad-followup/</link><guid isPermaLink="false">59ea3a83c57f960019ef9d6f</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 13 Aug 2017 19:57:14 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Somewhat appropriately, I finished and edited my <a href="http://harveynick.com/2017/07/30/thinking-about-going-ipad-mostly/">previous post about switching to an iPad for many of my computing use cases</a> <em>on</em> an iPad.  Luckily I have access to one which belongs to my employer and happens to be running the <a href="https://www.apple.com/ios/ios-11-preview/">iOS 11 Beta</a>. Since I was traveling at the time, I could also grab the <a href="http://amzn.to/2vAa1tc">Magic Keyboard</a> which usually sits on my desk for typing purposes. Here are some observations.</p>
<p>First of all: this obviously wasn’t an exact facsimile. The iPad Air 2 I was using has a slightly smaller screen and significantly less processing power than the iPad Pro I’m considering. It also doesn’t support the Apple Pencil, so that’s not something I couldn’t test in the field. Lastly, much as I love the Magic Keyboard (it might be my favourite keyboard ever, in fact) it’s far from an exact replication of the <a href="https://harveynick.com/2017/08/13/mostly-an-ipad-followup/iPad%20Pro%20Smart%20Keyboard">iPad Pro Smart Keyboard</a>. It’s a bit nicer to type on from what I can tell, it’s nowhere near as mobile, and it doesn’t attach to the device for more laptop like use.</p>
<p>With all of that in mind, the first thing I’d note is that the experience of typing text was really good. I did some work in <a href="https://ia.net/writer/">iA Writer</a>, then moved it into <a href="https://www.ulyssesapp.com">Ulysses</a> in the interests of trying something new (for the record I’m typing this in <a href="http://www.bear-writer.com">Bear</a> for much the same reason). All of this worked really nicely. Ulysses in particular came pretty close to pulling me all the way over.</p>
<p>When it came to editing it was great to be able to move to a chair, flip the device to portrait and change the context as much as possible. I found that the on-screen soft keyboard was more than equal to making minor edits here, especially with the improved keyboard in iOS 11.</p>
<p>One thing which didn’t work quite so well was moving large chunks of text around. This is pretty trivial with a mouse or trackpad. It’s also very easy with a hard keyboard (and its curser keys). With iOS’s on screen selection system I found this much more awkward. Hopefully this is just a matter of inexperience and I’m actually missing a key insight which will make it much easier. Perhaps there will be improvements when apps start using the new iOS 11 APIs.</p>
<p>Moving content between apps is still pretty awkward, but again I expect iOS 11 will improve this. Moving your attention between different apps I actually find to work really well on the iPad, though. The much more formal system of how apps appear on the screen seems to be easier for me to handle intuitively (or at least reflexively).</p>
<p>Ulysses has the built in functionality to export directly to Medium and Wordpress. It would be really good if it had the same functionality for <a href="https://ghost.org">Ghost</a>, because the Ghost web UI really doesn’t scale well to the iPad sized screen. It seems to fall into something of a blind spot. Hopefully the new editor in <a href="https://blog.ghost.org/1-0/">Ghost 1.0</a> will improve this, but it doesn’t seem to have been rolled out to my blog yet. In fact thanks to Wordpress’s very solid mobile app, this whole process would have been <em>much</em> simpler if I was using <a href="https://wordpress.org">Wordpress</a><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p>
<p>I also spent some time using <a href="https://affinity.serif.com/en-gb/photo/ipad/">Affinity Photo</a> to develop <a href="https://en.wikipedia.org/wiki/Raw_image_format">RAW files</a> taken with my mirrorless camera. The short summery is that it works really, really well on the iPad. It was a little slow at times, but given I was using it on the lowest specced device it’s compatible with that’s quite understandable. If I was doing anything more delicate than developing I think the Apple Pencil would have been very nice to have. Doing this kind of work whilst comfortable sitting crosslegged in a sofa is very nice indeed.</p>
<p>The experience of getting the photos out of the Mirrorless camera and onto the iPad was also pretty good. The <a href="http://amzn.to/2uSOERL">iPad SD Card based connection kit</a> does its job really well. I suspect the <a href="http://amzn.to/2wV6m8N">USB based version</a> is probably the better option most of the time, though. I’ll probably switch to that and put up with also needing to carry an additional cable. Whilst it was straight forward it was a little slow. Wireless transfer of RAW files (which tend to be <em>big</em>) would be way more convenient, but slower still. It might be worth it.</p>
<p>A quick aside about the way in which the Photos app stores and displays RAW files: If you shoot in RAW+JPEG (so your camera outputs both a RAW file and a developed JPEG), Photos displays this as a single photo. Any changes you make are applied only to the JPEG. Photos itself barely even acknowledges that the RAW exists. Affinity will open the RAW file by default when you import from your photo library , but it would be awesome if it could also act as an extension and allow you to <em>redevelop</em> the RAW file from with Photos.</p>
<p>I also used it as a media device during the flight<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. So much nicer than the built in option. The Netflix UI is lightyears ahead of any of the grotesque “entertainment systems” I’ve encountered on a plane. The iPad screen is much better than any in-seat screen. As an added bonus: When the pilot or cabin crew decide it’s time to wax lyrical, you get to choose whether to listen, or stick with the entertainment you <em>chose</em>.</p>
<p>As final thoughts: mobility and battery life were both glorious. With a SIM card and a decent international plan it could be amazing.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>In actually fact, thanks to really solid tools like the Working Copy git client, a lot of this might even have been easier using Jekyll and GitHub Pages. I still really like Ghost and intend to keep using it for this blog, but I can’t deny that frustrations like this keep pilling up. I’m going to be looking at other options for some other side projects I have in mind. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>I watched the “Netflix Original” move Spectral, in case you’re interested. I would recommend it for you if you (like me) are a fan of a) very well made B movies; or b) Doctor Who, with which it shares an attitude to “science”. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item><item><title><![CDATA[Thinking About Going iPad... Mostly]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Going “iPad only” is all the rage in some circles. I don't think it would really work for me. iOS still has too many limitations for that. But I do like the idea of using an iPad as my main “carry around” machine. Right now I use a first generation</p></div>]]></description><link>https://harveynick.com/2017/07/30/thinking-about-going-ipad-mostly/</link><guid isPermaLink="false">59ea3a83c57f960019ef9d6e</guid><category><![CDATA[Blogger]]></category><category><![CDATA[Hacker]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 30 Jul 2017 12:00:00 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Going “iPad only” is all the rage in some circles. I don't think it would really work for me. iOS still has too many limitations for that. But I do like the idea of using an iPad as my main “carry around” machine. Right now I use a first generation MackBook Adorable for this purpose. I take it on holiday and bring it with me to coffee shops (and occasionally the office) when I have use for it. But an iPad is smaller, lighter, and more versatile in form (if not in function). It also uses the same charger as my phone, which I consider a win.</p>
<p>The MacBook is also very small and light, but the issue is that a lot of the software built for it was not designed with the “small” part in mind. A lot of Mac apps were clearly designed to run on a 27&quot; iMac screen and perhaps scale down to a 15&quot; MacBook Pro. The 12&quot; screen of the MacBook is not what the designers had in mind, and so the apps end up cramped and inelegant. Due to the windowing user interface, software for the Mac must be capable of being displayed at almost any size. It can't possibly be optimized for all of them.</p>
<p>It might seem natural that if the 12&quot; Screen of the MacBook feels cramped, then the 9&quot; or 10.5&quot; screen of the (non tea-tray sized) iPad would be even more so. In practice this is generally not the case. The limited sizes an iOS app can be displayed in are known at design time. Thus they can be taken into account from the get go, and iPad apps (even complicated ones) can achieve a high level of elegance.</p>
<p>I can think of four key use cases I have for my MacBook Adorable, which any possible replacement would have to cover:</p>
<ul>
<li>Content consumption;</li>
<li>Writing;</li>
<li>Image Editing;</li>
<li>Coding.</li>
</ul>
<p>Content consumption is one area in which the iPad is basically peerless. Technically there isn't anything you can do on a iPad which you can't do on a laptop, but often times it's much more pleasant on the iPad.</p>
<p>If you want to watch Netflix or Amazon Video, for example, the iPad app gives a much cleaner interface than the browser, and allows you to download (some) TV shows and movies to watch offline later.</p>
<p>There are exceptions to this, of course. Some websites don't work well on Safari mobile and so can't be viewed on the iPad at all. Any website which uses Flash, for example, but that's increasingly rare. The Kindle app (and other eReaders, for that matter) on the iPad is fantastic for viewing cookbooks and  textbooks[^1]. But an ePaper Kindle is significantly more comfortable when it comes to novels and other long form writing.</p>
<p>For the writing use case, I think a tiny laptop and an iPad with a keyboard are on a pretty equal footing. I use <a href="https://ia.net/writer/">IA Writer</a> for this purpose. It's available on both platforms, so no issue there. It's also very minimal, so screen real estate is more or less a non-issue. If Matt Gemmell can <a href="http://mattgemmell.com/laptop-replacement/">write Novels on an iPad</a> I can probably manage to write blog posts.</p>
<p>Using a trackpad/mouse <em>is</em> more convenient than tapping the screen for quickly jumping around the text. However the lack of this might not actually be such a bad thing. It would force me to rely on the keyboard more, which is likely to increase my productivity in the long run.</p>
<p>Another bonus is the ability to remove the keyboard and rotate into portrait mode when editing. In my experience changing your context as much as possible is really good idea when editing. It stops me from seeing what I was trying to write, rather than what I <em>actually</em> wrote.</p>
<p>Image editing, when performed on a Mac, is one of those use cases which benefits from the largest screen possible. More pixels on the screen means you can see more of the pixels in your photo (obviously). Plus you have space for the myriad palettes and tools image editing tends to require.</p>
<p>I'm mainly thinking of two apps here: Adobe's ubiquitous <a href="http://www.adobe.com/uk/creativecloud/photography.html">LightRoom</a>, and Serif’s plucky newcomer <a href="https://affinity.serif.com/en-gb/">Affinity Photo</a>. There's not an exact one to one mapping between the two. Affinity Photo is more of a competitor to PhotoShop than LightRoom, but it can be purchased for a one off fee rather than requiring a subscription, so it's what I've tended to use when developing raw files and otherwise tweaking the images which come out of my camera.</p>
<p>As luck would have it, there are iPad versions of both of these apps. <a href="http://www.adobe.com/uk/products/lightroom-mobile.html">LightRoom Mobile</a> is mostly a companion to the desktop version, but appears pretty fully functional. <a href="https://affinity.serif.com/en-gb/photo/ipad/">Affinity Photo for iPad</a> is the whole shebang, though. It's seriously impressive. In both cases, the possibility of using an Apple Pencil for selection and making exact adjustments is very compelling.</p>
<p>The downside here is the lack of a good way of getting access to the image files for editing. Neither (currently) supports reading from external sources directly. LightRoom mobile appears to work best if you first upload the pictures to Adobe’s cloud via the desktop app, then pull them back down to the iPad. It can pull RAW files from the iPad’s camera roll, but not elegantly. Affinity Photo from iPad doesn’t seem to be able to see those RAW files at all, though. It wants you to upload them separately to cloud storage, and then pull them down individually. The workflow here is really not appealing, but I’m hopeful that new features in <a href="https://www.apple.com/uk/ios/ios-11-preview">iOS 11</a> will help here.</p>
<p>Coding is potentially a showstopper for the iPad. Much as I want Xcode for iPad to exist, it stubbornly persists in failing to. It's not that coding is completely impossible on the tablet. There exist very solid apps for programming in <a href="http://www.adobe.com/uk/products/lightroom-mobile.html">Python</a> and <a href="https://codea.io">Lua</a> for example. Unfortunately I want to code in Swift. There <em>is</em> the increasingly impressive Swift Playgrounds app. Unfortunately I want to work on a full App.</p>
<p>One possible solution is to use <a href="https://affinity.serif.com/en-gb/photo/ipad/">Dringend</a>, which provides a full App coding environment for Objective-C and Swift. However it also requires a Mac to be used as a build server in order to make this work. Another slight red flag is that the App hasn't been updated since January 2016. As things stand it wouldn't gain the benefits of iOS 11. But still, there is hope.</p>
<p>That said, if I have a decent PC back at home base then possibly I don't <em>need</em> to code on the iPad. Maybe I can work on coding side projects when I'm at home, but when I'm out and about I can do... other things. Like write, or work on photos. Or maybe just look up and enjoy the world. It's crazy, I know.</p>
<p>So... what's my conclusion? I really like the idea, and will probably go ahead with it... at some point. Right now I really don't want to spend the money, but when my MacBook Adorable hits end of life I think it might be the next thing for me. A reasonable iMac at home (maybe) and an iPad Pro for when I'm not at my desk. It seems like a good division of labour.</p>
<p>[^1] anything with pictures, or that you might want to flick around in, essentially.</p>
</div>]]></content:encoded></item><item><title><![CDATA[Should Have Paid for the Delivery, or: Value Your Time]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Just before the weekend I realised I needed a few items from Ikea at fairly short notice. Nothing complicated. A rug and some rails of the sort kitchen utensils dangle from. My in-progress kitchen would definitely remain in-progress without the latter. The obvious solution was the usual online shopping and</p></div>]]></description><link>https://harveynick.com/2017/07/16/should-have-paid-for-the-delivery-or-value-your-time/</link><guid isPermaLink="false">59ea3a83c57f960019ef9d6d</guid><category><![CDATA[Blogger]]></category><dc:creator><![CDATA[Nick Johnson]]></dc:creator><pubDate>Sun, 16 Jul 2017 21:29:52 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Just before the weekend I realised I needed a few items from Ikea at fairly short notice. Nothing complicated. A rug and some rails of the sort kitchen utensils dangle from. My in-progress kitchen would definitely remain in-progress without the latter. The obvious solution was the usual online shopping and home delivery combo. I fired up Ikea.com and lobbed everything I needed into the basket. I clicked checkout. Cost for delivery: £35, said the site. &quot;Daylight robbery!&quot; said I. I think you can see where this is going. More specifically: I think you can see where <em>I</em> was going.</p>
<p>Ikea is 60 minutes away from me by public transport. 30 minutes by car. Driving myself in a ZipCar was more logistics than I was looking for, so I decided public transport there, Uber back was my best option. Off I set. About two hours later I had a heavy yellow Ikea sack hanging from my shoulder (because obviously I also spotted a couple of other things we could <em>really</em> use). I started to manhandle the 2.5 metre rug off the shelf in the self service warehouse. I'm sure I knew how big the rug was beforehand, but it hadn't entirely registered until this point. It was beginning to dawn on me: &quot;this is silly.&quot;</p>
<p>In the end the 30 minute Uber back cost me £17.14. The 60 minute trip on public transport to get there in the first place cost £6.60. I spend around an hour trudging around Ikea. So the final tally was £23.74. I saved £11.26, but I spent 2 and a half hours of my life. In other words I payed myself almost exactly £4.50 an hour for my time.</p>
<p>I really like to think that my time is worth more than that. It's certainly worth more than that to <em>me</em>. I generally have a rule that if I can pay money to increase quality of life for myself or my partner I will do so without hesitation. I broke that rule spectacularly here.</p>
<p>I can handle the economy cabin for a long flight, but if it's overnight I will tend to upgrade to premium. Business would be nice but I generally can't afford it when paying for myself. An overnight flight in economy will usually leave me completely destroyed the next day. It will be several days and a lot of coffee before I'm back up to speed. It's worth it to me to pay the extra money and avoid that.</p>
<p>Another example is taking my shirts to the laundrette. It takes me something like 30 minutes to do a crappy job of ironing a shirt. But for £1.50 each my local laundrette will wash <em>and</em> iron them. For me it's an absolute no brainer<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p>
<p>In a world of Amazon Prime and public transport which feels free at point of use, £35 <em>sounded</em> like a lot of money for delivery. In fact I was thinking about it in entirely the wrong way, and my maths was deeply flawed. You should value your time highly. Very highly. If someone is providing a service which is genuinely useful to you, then you should pay them for it. I should have been praising Ikea for charging for delivery, and not just disguising the cost elsewhere.</p>
<p>The longwinded upshot is this: Next time it comes up, I'm definitely paying Ikea for the damn delivery. Replace the words &quot;Ikea&quot; and &quot;delivery&quot; as appropriate.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Before someone says it: I'm not saying my time is worth more than the person ironing the shirts in the laundrette. I'm saying they'll iron the shirt in 3 minutes and do a <em>much</em> better job of it. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
</div>]]></content:encoded></item></channel></rss>